├── .gitattributes
├── .github
└── workflows
│ ├── publish-deno.yml
│ ├── publish-python.yml
│ ├── release-binary.yml
│ ├── rust-binary.yml
│ └── verify.yml
├── .gitignore
├── .vscode
├── extensions.json
└── settings.shared.json
├── CHANGELOG.md
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── deno.lock
├── json-schema.d.ts
├── mise.toml
├── mise.windows.toml
├── renovate.json
├── schemas
├── WebViewMessage.json
├── WebViewOptions.json
├── WebViewRequest.json
└── WebViewResponse.json
├── scripts
├── generate-schema
│ ├── debug.ts
│ ├── gen-helpers.ts
│ ├── gen-python.test.ts
│ ├── gen-python.ts
│ ├── gen-typescript.ts
│ ├── index.ts
│ ├── parser.test.ts
│ ├── parser.ts
│ └── printer.ts
└── sync-versions.ts
├── sg
└── rules
│ ├── .gitkeep
│ └── no-mixed-enums.yml
├── sgconfig.yml
└── src
├── bin
├── generate_schemas.rs
└── webview.rs
├── clients
├── deno
│ ├── README.md
│ ├── deno.json
│ ├── deno.lock
│ ├── examples
│ │ ├── ipc.ts
│ │ ├── load-html.ts
│ │ ├── load-url.ts
│ │ ├── simple.ts
│ │ ├── tldraw.ts
│ │ └── window-size.ts
│ ├── main.ts
│ └── schemas.ts
└── python
│ ├── .python-version
│ ├── README.md
│ ├── examples
│ ├── ipc.py
│ ├── load_html.py
│ ├── load_url.py
│ ├── simple.py
│ └── window_size.py
│ ├── pyproject.toml
│ ├── src
│ └── justbe_webview
│ │ ├── __init__.py
│ │ ├── py.typed
│ │ └── schemas.py
│ └── uv.lock
└── lib.rs
/.gitattributes:
--------------------------------------------------------------------------------
1 | schemas/** linguist-generated=true
2 |
--------------------------------------------------------------------------------
/.github/workflows/publish-deno.yml:
--------------------------------------------------------------------------------
1 | name: Publish Deno Client
2 | on:
3 | workflow_run:
4 | workflows: ["Verify", "Release Rust Binary"]
5 | types:
6 | - completed
7 | branches: [main]
8 |
9 | jobs:
10 | publish:
11 | runs-on: ubuntu-latest
12 | defaults:
13 | run:
14 | working-directory: src/clients/deno
15 |
16 | permissions:
17 | contents: write
18 | id-token: write
19 |
20 | steps:
21 | - uses: actions/checkout@v4
22 |
23 | - name: Get current version
24 | id: current_version
25 | run: echo "version=$(grep '"version"' deno.json | awk '{ print $2 }' | sed 's/[",]//g')" >> $GITHUB_OUTPUT
26 |
27 | - name: Get published version
28 | id: published_version
29 | run: echo "version=$(npx jsr show @justbe/webview | head -n 2 | npx -q strip-ansi-cli | xargs | awk '{print $4}')" >> $GITHUB_OUTPUT
30 |
31 | - name: Publish package
32 | if: ${{ steps.current_version.outputs.version != steps.published_version.outputs.version }}
33 | run: npx jsr publish
34 | env:
35 | LATEST_VERSION: ${{ env.latest_version }}
36 |
37 | - name: Get latest version
38 | if: ${{ steps.current_version.outputs.version != steps.published_version.outputs.version }}
39 | id: latest_version
40 | run: echo "version=$(npx jsr show @justbe/webview | head -n 2 | npx -q strip-ansi-cli | xargs | awk '{print $4}')" >> $GITHUB_OUTPUT
41 |
42 | - name: Tag and push if versions differ
43 | if: ${{ steps.current_version.outputs.version != steps.published_version.outputs.version && steps.latest_version.outputs.version != steps.published_version.outputs.version }}
44 | run: |
45 | git config user.name github-actions
46 | git config user.email github-actions@github.com
47 | git tag -a deno-v${{ steps.latest_version.outputs.version }} -m "Release ${{ steps.latest_version.outputs.version }}"
48 | git push origin deno-v${{ steps.latest_version.outputs.version }}
49 | env:
50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
51 |
--------------------------------------------------------------------------------
/.github/workflows/publish-python.yml:
--------------------------------------------------------------------------------
1 | name: Publish Python Client
2 | on:
3 | workflow_run:
4 | workflows: ["Verify", "Release Rust Binary"]
5 | types:
6 | - completed
7 | branches: [main]
8 | pull_request:
9 | paths:
10 | - 'src/clients/python/**'
11 |
12 | jobs:
13 | publish:
14 | runs-on: ubuntu-latest
15 | # Only run if the workflow_run was successful (for the main branch case)
16 | if: github.event_name == 'pull_request' || github.event.workflow_run.conclusion == 'success'
17 |
18 | defaults:
19 | run:
20 | working-directory: src/clients/python
21 |
22 | permissions:
23 | contents: write
24 | id-token: write
25 |
26 | steps:
27 | - uses: actions/checkout@v4
28 |
29 | - name: Install mise
30 | uses: jdx/mise-action@v2
31 |
32 | - name: Install tools
33 | run: mise install
34 |
35 | - name: Get current version
36 | id: current_version
37 | run: echo "version=$(grep '^version = ' pyproject.toml | cut -d'"' -f2)" >> $GITHUB_OUTPUT
38 |
39 | - name: Get published version
40 | id: published_version
41 | run: |
42 | if ! version=$(curl -sf https://pypi.org/pypi/justbe-webview/json | jq -r '.info.version // "0.0.0"'); then
43 | echo "Failed to fetch version from PyPI, using 0.0.0"
44 | version="0.0.0"
45 | fi
46 | echo "version=$version" >> $GITHUB_OUTPUT
47 |
48 | - name: Build package
49 | if: ${{ steps.current_version.outputs.version != steps.published_version.outputs.version || github.event_name == 'pull_request' }}
50 | run: uv build
51 |
52 | - name: Dry run publish to PyPI
53 | if: ${{ github.event_name == 'pull_request' }}
54 | run: |
55 | echo "Would publish version ${{ steps.current_version.outputs.version }} to PyPI"
56 | echo "Current published version: ${{ steps.published_version.outputs.version }}"
57 | echo "Package contents:"
58 | ls -l dist/
59 | echo "Archive contents:"
60 | tar tzf dist/*.tar.gz | sort
61 |
62 | - name: Publish to PyPI
63 | if: ${{ steps.current_version.outputs.version != steps.published_version.outputs.version && github.event_name == 'workflow_run' }}
64 | uses: pypa/gh-action-pypi-publish@release/v1
65 | with:
66 | packages-dir: src/clients/python/dist/
67 | verbose: true
68 |
69 | - name: Tag and push if versions differ
70 | if: ${{ steps.current_version.outputs.version != steps.published_version.outputs.version && github.event_name == 'workflow_run' }}
71 | run: |
72 | # Ensure the tag doesn't already exist
73 | if ! git rev-parse "python-v${{ steps.current_version.outputs.version }}" >/dev/null 2>&1; then
74 | git config user.name github-actions
75 | git config user.email github-actions@github.com
76 | git tag -a python-v${{ steps.current_version.outputs.version }} -m "Release ${{ steps.current_version.outputs.version }}"
77 | git push origin python-v${{ steps.current_version.outputs.version }}
78 | else
79 | echo "Tag python-v${{ steps.current_version.outputs.version }} already exists, skipping tag creation"
80 | fi
81 | env:
82 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/.github/workflows/release-binary.yml:
--------------------------------------------------------------------------------
1 | name: Release Rust Binary
2 |
3 | on:
4 | workflow_run:
5 | workflows: ["Rust Binary"]
6 | types:
7 | - completed
8 | branches: [main]
9 |
10 | jobs:
11 | release:
12 | name: Release
13 | runs-on: ubuntu-latest
14 | if: ${{ github.event.workflow_run.conclusion == 'success' }}
15 | permissions:
16 | contents: write
17 | steps:
18 | - uses: actions/checkout@v4
19 |
20 | - name: Download all artifacts
21 | uses: actions/download-artifact@v4
22 | with:
23 | github-token: ${{secrets.GITHUB_TOKEN}}
24 | run-id: ${{github.event.workflow_run.id}}
25 |
26 | - name: Check for artifacts
27 | id: check_artifacts
28 | run: |
29 | if [ -z "$(find . -name 'webview*')" ]; then
30 | echo "No artifacts found. Exiting successfully."
31 | echo "artifacts_found=false" >> $GITHUB_OUTPUT
32 | else
33 | echo "Artifacts found. Proceeding with release."
34 | echo "artifacts_found=true" >> $GITHUB_OUTPUT
35 | fi
36 |
37 | - name: Get version from Cargo.toml
38 | if: steps.check_artifacts.outputs.artifacts_found == 'true'
39 | id: get_version
40 | run: |
41 | VERSION=$(grep '^version =' Cargo.toml | sed 's/.*= *"//' | sed 's/".*//')
42 | echo "version=$VERSION" >> $GITHUB_OUTPUT
43 |
44 | - name: Create Release
45 | if: steps.check_artifacts.outputs.artifacts_found == 'true'
46 | uses: ncipollo/release-action@v1
47 | with:
48 | tag: webview-v${{ steps.get_version.outputs.version }}
49 | name: Release ${{ steps.get_version.outputs.version }}
50 | token: ${{ secrets.GITHUB_TOKEN }}
51 | prerelease: true
52 | skipIfReleaseExists: true
53 | artifacts: "release-binary-*/webview*"
54 | artifactErrorsFailBuild: true
55 |
--------------------------------------------------------------------------------
/.github/workflows/rust-binary.yml:
--------------------------------------------------------------------------------
1 | name: Rust Binary
2 |
3 | on:
4 | push:
5 | paths:
6 | - 'src/**/*rs'
7 | - 'Cargo.toml'
8 | - 'Cargo.lock'
9 |
10 | jobs:
11 | build:
12 | name: Build on ${{ matrix.os }} for ${{ matrix.target }}
13 | runs-on: ${{ matrix.os }}
14 | strategy:
15 | fail-fast: ${{ github.ref == 'refs/heads/main' }}
16 | matrix:
17 | include:
18 | - os: ubuntu-latest
19 | target: x86_64-unknown-linux-gnu
20 | binary_name: webview-linux
21 | platform: linux
22 | - os: ubuntu-latest
23 | target: x86_64-pc-windows-msvc
24 | binary_name: webview-windows
25 | platform: windows
26 | - os: macos-latest
27 | target: x86_64-apple-darwin
28 | binary_name: webview-mac
29 | platform: macos
30 | - os: macos-latest
31 | target: aarch64-apple-darwin
32 | binary_name: webview-mac-arm64
33 | platform: macos
34 | steps:
35 | - uses: actions/checkout@v4
36 |
37 | - uses: jdx/mise-action@v2
38 | env:
39 | RUSTUP_TARGET: ${{ matrix.target }}
40 | MISE_ENV: ${{ matrix.platform }}
41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
42 | with:
43 | cache_key_prefix: mise-${{ hashFiles('mise.toml') }}-${{ matrix.target }}
44 | experimental: true
45 |
46 | - name: Setup Rust cache
47 | uses: Swatinem/rust-cache@v2
48 | with:
49 | cache-on-failure: true
50 | shared-key: "binary-${{ matrix.target }}"
51 | workspaces: "."
52 | cache-directories: |
53 | ~/.cargo/registry/index
54 | ~/.cargo/registry/cache
55 | ~/.cargo/git/db
56 |
57 | - run: mise x -- rustup target add ${{ matrix.target }}
58 | - run: mise run ci:install-deps
59 |
60 |
61 | - name: Set build flags
62 | id: build_flags
63 | run: |
64 | if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
65 | echo "flags=--release" >> $GITHUB_OUTPUT
66 | echo "build_type=release" >> $GITHUB_OUTPUT
67 | else
68 | echo "flags=" >> $GITHUB_OUTPUT
69 | echo "build_type=debug" >> $GITHUB_OUTPUT
70 | fi
71 |
72 | - name: Build Linux
73 | if: matrix.target == 'x86_64-unknown-linux-gnu'
74 | run: |
75 | mise run build:rust -F transparent ${{ steps.build_flags.outputs.flags }} --target ${{ matrix.target }}
76 | mv target/${{ matrix.target }}/${{ steps.build_flags.outputs.build_type }}/webview ${{ matrix.binary_name }}
77 |
78 | mise run build:rust -F 'transparent devtools' ${{ steps.build_flags.outputs.flags }} --target ${{ matrix.target }}
79 | mv target/${{ matrix.target }}/${{ steps.build_flags.outputs.build_type }}/webview ${{ matrix.binary_name }}-devtools
80 |
81 | - name: Build macOS x86_64
82 | if: matrix.target == 'x86_64-apple-darwin'
83 | run: |
84 | mise run build:rust ${{ steps.build_flags.outputs.flags }} --target ${{ matrix.target }}
85 | mv target/${{ matrix.target }}/${{ steps.build_flags.outputs.build_type }}/webview ${{ matrix.binary_name }}
86 |
87 | mise run build:rust -F transparent ${{ steps.build_flags.outputs.flags }} --target ${{ matrix.target }}
88 | mv target/${{ matrix.target }}/${{ steps.build_flags.outputs.build_type }}/webview ${{ matrix.binary_name }}-transparent
89 |
90 | mise run build:rust -F 'transparent devtools' ${{ steps.build_flags.outputs.flags }} --target ${{ matrix.target }}
91 | mv target/${{ matrix.target }}/${{ steps.build_flags.outputs.build_type }}/webview ${{ matrix.binary_name }}-devtools
92 |
93 | - name: Build (aarch64-apple-darwin)
94 | if: matrix.target == 'aarch64-apple-darwin'
95 | run: |
96 | SDKROOT=$(xcrun -sdk macosx --show-sdk-path) MACOSX_DEPLOYMENT_TARGET=$(xcrun -sdk macosx --show-sdk-platform-version) mise run build:rust ${{ steps.build_flags.outputs.flags }} --target ${{ matrix.target }}
97 | mv target/${{ matrix.target }}/${{ steps.build_flags.outputs.build_type }}/webview ${{ matrix.binary_name }}
98 |
99 | SDKROOT=$(xcrun -sdk macosx --show-sdk-path) MACOSX_DEPLOYMENT_TARGET=$(xcrun -sdk macosx --show-sdk-platform-version) mise run build:rust -F transparent ${{ steps.build_flags.outputs.flags }} --target ${{ matrix.target }}
100 | mv target/${{ matrix.target }}/${{ steps.build_flags.outputs.build_type }}/webview ${{ matrix.binary_name }}-transparent
101 |
102 | SDKROOT=$(xcrun -sdk macosx --show-sdk-path) MACOSX_DEPLOYMENT_TARGET=$(xcrun -sdk macosx --show-sdk-platform-version) mise run build:rust -F 'transparent devtools' ${{ steps.build_flags.outputs.flags }} --target ${{ matrix.target }}
103 | mv target/${{ matrix.target }}/${{ steps.build_flags.outputs.build_type }}/webview ${{ matrix.binary_name }}-devtools
104 |
105 | - name: Build (Windows)
106 | if: matrix.target == 'x86_64-pc-windows-msvc'
107 | env:
108 | MISE_ENV: windows
109 | run: |
110 | mise run build:rust -F transparent ${{ steps.build_flags.outputs.flags }} --target ${{ matrix.target }}
111 | mv target/${{ matrix.target }}/${{ steps.build_flags.outputs.build_type }}/webview.exe ${{ matrix.binary_name }}.exe
112 |
113 | mise run build:rust -F 'transparent devtools' ${{ steps.build_flags.outputs.flags }} --target ${{ matrix.target }}
114 | mv target/${{ matrix.target }}/${{ steps.build_flags.outputs.build_type }}/webview.exe ${{ matrix.binary_name }}-devtools.exe
115 |
116 | - name: Upload artifact
117 | uses: actions/upload-artifact@v4
118 | with:
119 | name: ${{ steps.build_flags.outputs.build_type }}-binary-${{ matrix.target }}
120 | path: ${{ matrix.binary_name }}*
121 |
--------------------------------------------------------------------------------
/.github/workflows/verify.yml:
--------------------------------------------------------------------------------
1 | name: Verify
2 | on:
3 | push:
4 |
5 | jobs:
6 | lint:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v4
10 |
11 | - uses: jdx/mise-action@v2
12 | with:
13 | cache_key_prefix: mise-{{hashFiles('mise.toml')}}
14 | experimental: true
15 |
16 | - name: Setup Rust cache
17 | uses: Swatinem/rust-cache@v2
18 | with:
19 | cache-on-failure: true
20 | shared-key: "lint"
21 |
22 | - name: Lint
23 | run: mise lint
24 |
25 | publishable:
26 | runs-on: ubuntu-latest
27 | steps:
28 | - uses: actions/checkout@v4
29 |
30 | - uses: jdx/mise-action@v2
31 | with:
32 | experimental: true
33 |
34 | - name: Passes publish checks
35 | run: mise run verify-publish:*
36 |
37 | codegen-up-to-date:
38 | runs-on: ubuntu-latest
39 | steps:
40 | - uses: actions/checkout@v4
41 |
42 | - uses: jdx/mise-action@v2
43 | with:
44 | cache_key_prefix: mise-{{hashFiles('mise.toml')}}
45 | experimental: true
46 |
47 | - name: Setup Rust cache
48 | uses: Swatinem/rust-cache@v2
49 | with:
50 | cache-on-failure: true
51 | shared-key: "codegen"
52 |
53 | - name: Run codegen
54 | run: mise run gen
55 |
56 | - name: Check for changed files
57 | run: |
58 | if [[ -n $(git status --porcelain) ]]; then
59 | echo "Files have changed after running tests:"
60 | git status --porcelain
61 | git diff
62 | exit 1
63 | fi
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | _test*
3 | .DS_Store
4 | __pycache__
5 | node_modules
6 |
7 | .vscode/*
8 | !.vscode/*.shared.json
9 | !.vscode/extensions.json
10 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
4 |
5 | // List of extensions which should be recommended for users of this workspace.
6 | "recommendations": [
7 | "hverlin.mise-vscode",
8 | "denoland.vscode-deno",
9 | "charliermarsh.ruff",
10 | "Swellaby.workspace-config-plus"
11 | ],
12 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace.
13 | "unwantedRecommendations": [
14 |
15 | ]
16 | }
--------------------------------------------------------------------------------
/.vscode/settings.shared.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.semanticHighlighting.enabled": true,
3 | "[typescript]": {
4 | "editor.defaultFormatter": "denoland.vscode-deno"
5 | },
6 | "[json]": {
7 | "editor.defaultFormatter": "denoland.vscode-deno"
8 | },
9 | "deno.enablePaths": [
10 | "scripts",
11 | "src/clients/deno"
12 | ],
13 | "python.analysis.typeCheckingMode": "strict",
14 | "python.analysis.extraPaths": [
15 | "${workspaceFolder}/src/clients/python/.venv/lib/python3.*/site-packages"
16 | ],
17 | "mise.configureExtensionsAutomatically": true
18 | }
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 0.0.4 Python Client; 1.0.1-rc.2 Deno Client; 0.3.1 binary -- 2025-03-05
4 |
5 | - Fixed a bug that caused webview to fail to open on linux
6 | - Upgraded various native dependencies
7 |
8 | ## 0.0.3 Python Client -- 2025-03-01
9 |
10 | - Fixed a bug where the windows binary path was still incorrect (thanks @daidalvi)
11 |
12 | ## 0.0.2 Python Client; 1.0.1-rc.1 Deno Client; 0.3.0 binary -- 2025-02-23
13 |
14 | Binary
15 |
16 | - Updated wry to 0.49.0
17 | - Updated tao to 0.32.0
18 |
19 | Python Client
20 |
21 | - Updated webview binary to 0.3.0
22 | - Fixed readme
23 | - Fixed binary path being wrong
24 | - Fixed windows binary name being incorrectly constructed
25 | - Fixed an issue where events weren't being acknowledged
26 |
27 | Deno Client
28 |
29 | - Updated webview to 0.3.0
30 |
31 | ## 0.0.1 Python Client; 0.2.0 binary -- 2025-02-18
32 |
33 | - Initial release of the python client
34 |
35 | ## 1.0.0-rc.1 Deno Client; 0.2.0 binary -- 2025-02-17
36 |
37 | - Added new logging that can be triggered with the `LOG_LEVEL` environment variable
38 |
39 | - [BREAKING] Changed some typenames/zod schemas not to include `Webview` in the name.
40 | - [BREAKING] Updated code generation to support multiple clients which necessitated a breaking change for the Deno client.
41 |
42 | ```diff
43 | using webview = await createWebView({
44 | title: "Simple",
45 | devtools: true,
46 | + load: {
47 | html: "
Hello, World! ",
48 | + },
49 | initializationScript:
50 | "console.log('This is printed from initializationScript!')",
51 | });
52 | ```
53 | `html` or `url` must be wrapped in an object and passed to `load`.
54 |
55 | ## 0.0.17 (binary 0.1.14) -- 2024-10-02
56 |
57 | - Add `webview.loadUrl` to load a new URL after the webview is initialized
58 | - Add the ability to specify headers when instantiating a webview with a URL
59 | - Add `userAgent` to `WebViewOptions` to construct a new webview with a custom user agent.
60 |
61 | ## 0.0.16 (binary 0.1.13) -- 2024-09-29
62 |
63 | - Add `initializationScript` to `WebViewOptions`. Allows providing JS that runs before `onLoad`.
64 |
65 | ## 0.0.15 (binary 0.1.12) -- 2024-09-28
66 |
67 | - Pages loaded with `html` are now considered to be in a secure context.
68 | - When creating a webview with `html` or calling `webview.loadHtml()` the webview now has a default origin which can be changed via the `origin` parameter
69 | - Improved type generation to output more doc strings and documented more code
70 | - Update TLDraw example with a persistence key
71 |
72 | ## 0.0.14 (binary 0.1.11) -- 2024-09-26
73 |
74 | - fix an issue where arm64 macs weren't downloading the correct binary
75 |
76 | ## 0.0.13 (binary 0.1.11) -- 2024-09-26
77 |
78 | - added `webview.loadHtml(...)`
79 |
80 | ## 0.0.12 (binary 0.1.10) -- 2024-09-26
81 |
82 | BREAKING CHANGES
83 |
84 | - `WebViewOptions` `accept_first_mouse` is now `acceptFirstMouse`
85 | - `WebViewOptions` `fullscreen` was removed in favor of `size`
86 |
87 | Additions
88 |
89 | - The webview size can be altered by providing `WebViewOptions` `size` as either `"maximized"`, `"fullscreen"`, or `{ width: number, height: number }`
90 | - added `webview.maximize()`
91 | - added `webview.minimize()`
92 | - added `webview.fullscreen()`
93 | - added `webview.getSize()`
94 | - added `webview.setSize({ ... })`
95 |
96 | Fixes
97 |
98 | - `webview.on` and `webivew.once` had their types improved to actually return the result of their event payload
99 |
100 | Misc
101 |
102 | - Tao updated to v0.30.2
103 | - Wry upgraded to v0.45.0
104 |
105 | ## 0.0.11 (binary 0.1.9) -- 2024-09-23
106 |
107 | - Adds more doc comments
108 |
109 | ## 0.0.10 (binary 0.1.9) -- 2024-09-23
110 |
111 | - Adds an `ipc` flag to enable sending messages from the webview back to the host deno process.
112 | - Adds an IPC example
113 | - Updates notifications to pass message bodies through
114 |
115 | ## 0.0.9 (binary 0.1.8) -- 2024-09-23
116 |
117 | - Adds a `getVersion` method to `Webview` that returns the binary version.
118 | - Adds a check on startup that compares the expected version to the current version.
119 | - Adds slightly more graceful error handling to deserialization errors in the deno code.
120 |
121 | ## 0.0.8 (binary 0.1.7) -- 2024-09-23
122 |
123 | NOTE: The binary version was bumped this release, but it doesn't actually have changes.
124 | This is just me forcing a new release to be published.
125 |
126 | - Fixed the release URL to ensure the download comes from the correct place
127 |
128 | ## 0.0.7 (binary 0.1.6) -- 2024-09-23
129 |
130 | - Added this changelog
131 | - Add the ability to show and hide the webview window via `.setVisibility(true/false)`
132 | - Added the ability to check if the webview window is visible via `.isVisible()`
133 |
134 | ## 0.0.6 (binary 0.1.5) -- 2024-09-23
135 |
136 | - Fixed a bug where `.on` and `.once` weren't firing for the webview's lifecycle events
137 |
138 | ## 0.0.5 (binary 0.1.5) -- 2024-09-23
139 |
140 | - Improved type generation to avoid publishing slow types to JSR
141 |
142 | ## 0.0.4 (binary 0.1.5) -- 2024-09-22
143 |
144 | - Added examples, doc comments to code
145 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "webview"
3 | version = "0.3.0"
4 | edition = "2021"
5 |
6 | [profile.release]
7 | strip = true
8 | lto = true
9 | opt-level = "z"
10 | codegen-units = 1
11 |
12 | [dependencies]
13 | serde = { version = "1", features = ["derive"] }
14 | serde_json = "1"
15 | tao = "0.32.0"
16 | wry = "0.51.0"
17 | schemars = "0.8.21"
18 | parking_lot = "0.12"
19 | actson = "2.0.0"
20 | tracing = "0.1"
21 | tracing-subscriber = { version = "0.3", features = ["env-filter"] }
22 |
23 | [features]
24 | transparent = ["wry/transparent"]
25 | devtools = ["wry/devtools"]
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2024 Justin Bennett
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # @justbe/webview
2 |
3 | A light, cross-platform library for building web-based desktop apps. The project consists of a Rust backend that provides the core webview functionality, with multiple client libraries available for different languages and runtimes.
4 |
5 | ## Available Clients
6 |
7 | - [Deno Client](src/clients/deno/README.md) - Build desktop apps using Deno and TypeScript
8 | - [Python Client](src/clients/python/README.md) - Build desktop apps using Python
9 |
10 | ## Architecture
11 |
12 | This project is structured into two main components:
13 |
14 | 1. A Rust backend that provides the core webview functionality, compiled into a native binary for each platform
15 | 2. Client libraries that interface with the binary, available for multiple languages
16 |
17 | Each client library handles binary management, communication with the webview process over stdio (standard input/output), and provides a idiomatic API for its respective language/runtime.
18 |
19 | ## Binary Management
20 |
21 | When using any of the clients, they will check for the required binary for interfacing with the OS's webview. If it doesn't exist, it downloads it to a cache directory and executes it. The specific behavior and permissions required may vary by client - please see the respective client's documentation for details.
22 |
23 | ### Using a Custom Binary
24 |
25 | All clients support using a custom binary via the `WEBVIEW_BIN` environment variable. If present and allowed, this will override the default binary resolution process in favor of the path specified.
26 |
27 | ## Examples
28 |
29 |
30 | Deno Example
31 |
32 | ```typescript
33 | import { createWebView } from "jsr:@justbe/webview";
34 |
35 | using webview = await createWebView({
36 | title: "Example",
37 | html: "Hello, World! ",
38 | devtools: true
39 | });
40 |
41 | webview.on("started", async () => {
42 | await webview.openDevTools();
43 | await webview.eval("console.log('This is printed from eval!')");
44 | });
45 |
46 | await webview.waitUntilClosed();
47 | ```
48 |
49 |
50 |
51 |
52 | Python Example
53 |
54 | ```python
55 | import asyncio
56 | from justbe_webview import WebView, WebViewOptions, WebViewContentHtml, WebViewNotification
57 |
58 | async def main():
59 | config = WebViewOptions(
60 | title="Example",
61 | load=WebViewContentHtml(html="Hello, World! "),
62 | devtools=True
63 | )
64 |
65 | async with WebView(config) as webview:
66 | async def handle_start(event: WebViewNotification):
67 | await webview.open_devtools()
68 | await webview.eval("console.log('This is printed from eval!')")
69 |
70 | webview.on("started", handle_start)
71 |
72 | if __name__ == "__main__":
73 | asyncio.run(main())
74 | ```
75 |
76 |
77 |
78 | ## Contributing
79 |
80 | This project uses [mise](https://mise.jdx.dev/) to manage runtimes (like deno, python, rust) and run scripts. If you'd like to contribute, you'll need to install it.
81 |
82 | Use the `mise tasks` command to see what you can do.
--------------------------------------------------------------------------------
/deno.lock:
--------------------------------------------------------------------------------
1 | {
2 | "version": "4",
3 | "specifiers": {
4 | "jsr:@std/fs@^1.0.3": "1.0.6",
5 | "jsr:@std/path@^1.0.6": "1.0.8",
6 | "jsr:@std/path@^1.0.8": "1.0.8",
7 | "jsr:@std/ulid@1": "1.0.0",
8 | "npm:ts-pattern@*": "5.6.0",
9 | "npm:zod@^3.23.8": "3.23.8"
10 | },
11 | "jsr": {
12 | "@std/fs@1.0.6": {
13 | "integrity": "42b56e1e41b75583a21d5a37f6a6a27de9f510bcd36c0c85791d685ca0b85fa2",
14 | "dependencies": [
15 | "jsr:@std/path@^1.0.8"
16 | ]
17 | },
18 | "@std/path@1.0.8": {
19 | "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be"
20 | },
21 | "@std/ulid@1.0.0": {
22 | "integrity": "d41c3d27a907714413649fee864b7cde8d42ee68437d22b79d5de4f81d808780"
23 | }
24 | },
25 | "npm": {
26 | "ts-pattern@5.6.0": {
27 | "integrity": "sha512-SL8u60X5+LoEy9tmQHWCdPc2hhb2pKI6I1tU5Jue3v8+iRqZdcT3mWPwKKJy1fMfky6uha82c8ByHAE8PMhKHw=="
28 | },
29 | "zod@3.23.8": {
30 | "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="
31 | }
32 | },
33 | "remote": {
34 | "https://deno.land/std@0.190.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462",
35 | "https://deno.land/std@0.190.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3",
36 | "https://deno.land/std@0.190.0/fs/_util.ts": "579038bebc3bd35c43a6a7766f7d91fbacdf44bc03468e9d3134297bb99ed4f9",
37 | "https://deno.land/std@0.190.0/fs/walk.ts": "920be35a7376db6c0b5b1caf1486fb962925e38c9825f90367f8f26b5e5d0897",
38 | "https://deno.land/std@0.190.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0",
39 | "https://deno.land/std@0.190.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b",
40 | "https://deno.land/std@0.190.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0",
41 | "https://deno.land/std@0.190.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000",
42 | "https://deno.land/std@0.190.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1",
43 | "https://deno.land/std@0.190.0/path/mod.ts": "ee161baec5ded6510ee1d1fb6a75a0f5e4b41f3f3301c92c716ecbdf7dae910d",
44 | "https://deno.land/std@0.190.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d",
45 | "https://deno.land/std@0.190.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1",
46 | "https://deno.land/std@0.190.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba"
47 | },
48 | "workspace": {
49 | "dependencies": [
50 | "jsr:@std/fs@^1.0.3",
51 | "jsr:@std/path@^1.0.6",
52 | "jsr:@std/ulid@1",
53 | "npm:type-fest@^4.26.1",
54 | "npm:zod@^3.23.8"
55 | ]
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/json-schema.d.ts:
--------------------------------------------------------------------------------
1 | export type JSONSchemaTypeName =
2 | | "string"
3 | | "number"
4 | | "integer"
5 | | "boolean"
6 | | "object"
7 | | "array"
8 | | "null";
9 |
10 | /**
11 | * Primitive type
12 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.1.1
13 | */
14 | export type JSONSchemaType =
15 | | string //
16 | | number
17 | | boolean
18 | | { [key: string]: JSONSchemaType }
19 | | JSONSchemaType[]
20 | | null;
21 |
22 | /**
23 | * JSON Schema v7
24 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01
25 | */
26 | export interface JSONSchema {
27 | $id?: string;
28 | $ref?: string;
29 | $schema?: string;
30 | $comment?: string;
31 |
32 | /**
33 | * @see https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-00#section-8.2.4
34 | * @see https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-00#appendix-A
35 | */
36 | $defs?: {
37 | [key: string]: JSONSchema;
38 | };
39 |
40 | /**
41 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.1
42 | */
43 | type?: JSONSchemaTypeName | JSONSchemaTypeName[];
44 | enum?: JSONSchemaType[];
45 | const?: JSONSchemaType;
46 |
47 | /**
48 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.2
49 | */
50 | multipleOf?: number;
51 | maximum?: number;
52 | exclusiveMaximum?: number;
53 | minimum?: number;
54 | exclusiveMinimum?: number;
55 |
56 | /**
57 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.3
58 | */
59 | maxLength?: number;
60 | minLength?: number;
61 | pattern?: string;
62 |
63 | /**
64 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.4
65 | */
66 | items?: JSONSchema | JSONSchema[];
67 | additionalItems?: JSONSchema;
68 | maxItems?: number;
69 | minItems?: number;
70 | uniqueItems?: boolean;
71 | contains?: JSONSchema;
72 |
73 | /**
74 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.5
75 | */
76 | maxProperties?: number;
77 | minProperties?: number;
78 | required?: string[];
79 | properties?: {
80 | [key: string]: JSONSchema;
81 | };
82 | patternProperties?: {
83 | [key: string]: JSONSchema;
84 | };
85 | additionalProperties?: JSONSchema;
86 | dependencies?: {
87 | [key: string]: JSONSchema | string[];
88 | };
89 | propertyNames?: JSONSchema | undefined;
90 |
91 | /**
92 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.6
93 | */
94 | if?: JSONSchema;
95 | then?: JSONSchema;
96 | else?: JSONSchema;
97 |
98 | /**
99 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.7
100 | */
101 | allOf?: JSONSchema[];
102 | anyOf?: JSONSchema[];
103 | oneOf?: JSONSchema[];
104 | not?: JSONSchema;
105 |
106 | /**
107 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-7
108 | */
109 | format?: string;
110 |
111 | /**
112 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-8
113 | */
114 | contentMediaType?: string;
115 | contentEncoding?: string;
116 |
117 | /**
118 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-9
119 | */
120 | definitions?: {
121 | [key: string]: JSONSchema;
122 | };
123 |
124 | /**
125 | * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-10
126 | */
127 | title?: string | undefined;
128 | description?: string | undefined;
129 | default?: JSONSchemaType | undefined;
130 | readOnly?: boolean | undefined;
131 | writeOnly?: boolean | undefined;
132 | examples?: JSONSchemaType | undefined;
133 | }
134 |
--------------------------------------------------------------------------------
/mise.toml:
--------------------------------------------------------------------------------
1 | [tools]
2 | deno = "2.2.1"
3 | "npm:@ast-grep/cli" = "0.38.3"
4 | rust = { version = "1.78.0", postinstall = "rustup component add rustfmt clippy rust-analyzer" }
5 | ruff = "0.9.1"
6 | uv = "0.6.2"
7 |
8 | [settings]
9 | experimental = true
10 | pin = true
11 |
12 | [tasks."ci:install-deps"]
13 | hide = true
14 | description = "Install CI dependencies (only runs on CI)"
15 | run = """
16 | {% if env.CI and os() == "linux" %}
17 | sudo apt-get update
18 | sudo apt-get install -y libwebkit2gtk-4.1-dev
19 | {% endif %}
20 | """
21 |
22 | [tasks.sync-versions]
23 | description = "Update all version references"
24 | run = "deno run -A scripts/sync-versions.ts"
25 |
26 | ## Gen
27 |
28 | [tasks."gen:rust"]
29 | depends = ["ci:install-deps"]
30 | description = "Generate JSON schemas from the rust code"
31 | run = "cargo run --bin generate_schemas"
32 | sources = ["src/**/*.rs", "Cargo.toml", "Cargo.lock"]
33 | outputs = ["schemas/*.json"]
34 |
35 | [tasks."gen:deno"]
36 | description = "Generate the deno client"
37 | run = "deno run -A scripts/generate-schema/index.ts --language typescript"
38 | depends = ["gen:rust"]
39 | sources = ["schemas/*", "scripts/generate-schema.ts"]
40 | outputs = ["src/clients/deno/schemas/*.ts"]
41 |
42 | [tasks."gen:python"]
43 | description = "Generate the python client"
44 | run = "deno run -A scripts/generate-schema/index.ts --language python"
45 | depends = ["gen:rust"]
46 | sources = ["schemas/*", "scripts/generate-schema.ts"]
47 | outputs = ["src/clients/python/src/justbe_webview/schemas/*.py"]
48 |
49 | ## Debug
50 |
51 | [tasks."print-schema"]
52 | description = "Prints a simplified version of the schema"
53 | usage = '''
54 | arg "[schema]" help="The schema to print; prints all if not provided"
55 | '''
56 | run = "deno run -A scripts/generate-schema/debug.ts {{arg(name=\"schema\")}}"
57 |
58 | ## Publishing
59 |
60 | [tasks."verify-publish:deno"]
61 | description = "Verify the deno client is pulishable"
62 | dir = "src/clients/deno"
63 | run = "deno publish --dry-run"
64 |
65 | [tasks.gen]
66 | description = "Run all code gen tasks"
67 | depends = ["gen:*"]
68 |
69 | ## Build
70 |
71 | [tasks."build:rust"]
72 | description = "Build the webview binary"
73 | run = """
74 | {% set xwin = '' %}
75 | {% set features = '' %}
76 | {% if get_env(name='MISE_ENV', default='') == 'windows' %}
77 | {% set xwin = ' xwin' %}
78 | {% endif %}
79 | {% if get_env(name='CI', default='') != 'true' %}
80 | {% set features = ' --features transparent,devtools' %}
81 | {% endif %}
82 | cargo{{xwin}} build --bin webview{{features}}
83 | """
84 | sources = ["src/**/*.rs", "Cargo.toml", "Cargo.lock"]
85 | outputs = ["target/debug/webview"]
86 | depends = ["gen:rust"]
87 |
88 | [tasks."build:deno"]
89 | description = "Run code gen for deno and ensure the binary is built"
90 | depends = ["gen:deno", "build:rust"]
91 |
92 | [tasks."build:python"]
93 | description = "Run code gen for python and ensure the binary is built"
94 | depends = ["gen:python", "build:rust"]
95 |
96 | [tasks.build]
97 | description = "Build all targets"
98 | depends = ["build:*"]
99 |
100 | ## Lint
101 |
102 | [tasks."lint:rust"]
103 | description = "Run clippy against rust code"
104 | depends = ["ci:install-deps"]
105 | run = ["cargo fmt --check", "cargo clippy"]
106 |
107 | [tasks."lint:deno"]
108 | description = "Run deno lint"
109 | dir = "src/clients/deno"
110 | run = ["deno lint", "deno check ."]
111 |
112 | [tasks."lint:ast-grep"]
113 | description = "Run ast-grep lint"
114 | run = """
115 | {% set format = '' %}
116 | {% if get_env(name='CI', default='') == 'true' %}
117 | {% set format = ' --format=github' %}
118 | {% endif %}
119 | sg scan {{format}} .
120 | """
121 |
122 | [tasks."lint"]
123 | description = "Run all linting tasks"
124 | depends = ["lint:*"]
125 |
126 | ## Example
127 |
128 | [tasks."example:python"]
129 | description = "Run a python example"
130 | depends = ["build:python"]
131 | dir = "src/clients/python"
132 | run = "uv run -n examples/{{arg(name=\"example\")}}.py"
133 | env = { LOG_LEVEL = "debug", WEBVIEW_BIN = "../../../target/debug/webview" }
134 |
135 | [tasks."example:deno"]
136 | description = "Run a deno example"
137 | depends = ["build:deno"]
138 | env = { LOG_LEVEL = "debug", WEBVIEW_BIN = "../../../target/debug/webview" }
139 | run = "deno run -E -R -N --allow-run examples/{{arg(name=\"example\")}}.ts"
140 | dir = "src/clients/deno"
141 |
--------------------------------------------------------------------------------
/mise.windows.toml:
--------------------------------------------------------------------------------
1 | [tools]
2 | "cargo:cargo-xwin" = "0.18.3"
3 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:recommended"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/schemas/WebViewMessage.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "title": "Message",
4 | "description": "Complete definition of all outbound messages from the webview to the client.",
5 | "oneOf": [
6 | {
7 | "type": "object",
8 | "required": [
9 | "$type",
10 | "data"
11 | ],
12 | "properties": {
13 | "$type": {
14 | "type": "string",
15 | "enum": [
16 | "notification"
17 | ]
18 | },
19 | "data": {
20 | "$ref": "#/definitions/Notification"
21 | }
22 | }
23 | },
24 | {
25 | "type": "object",
26 | "required": [
27 | "$type",
28 | "data"
29 | ],
30 | "properties": {
31 | "$type": {
32 | "type": "string",
33 | "enum": [
34 | "response"
35 | ]
36 | },
37 | "data": {
38 | "$ref": "#/definitions/Response"
39 | }
40 | }
41 | }
42 | ],
43 | "definitions": {
44 | "Notification": {
45 | "description": "Messages that are sent unbidden from the webview to the client.",
46 | "oneOf": [
47 | {
48 | "type": "object",
49 | "required": [
50 | "$type",
51 | "version"
52 | ],
53 | "properties": {
54 | "$type": {
55 | "type": "string",
56 | "enum": [
57 | "started"
58 | ]
59 | },
60 | "version": {
61 | "description": "The version of the webview binary",
62 | "type": "string"
63 | }
64 | }
65 | },
66 | {
67 | "type": "object",
68 | "required": [
69 | "$type",
70 | "message"
71 | ],
72 | "properties": {
73 | "$type": {
74 | "type": "string",
75 | "enum": [
76 | "ipc"
77 | ]
78 | },
79 | "message": {
80 | "description": "The message sent from the webview UI to the client.",
81 | "type": "string"
82 | }
83 | }
84 | },
85 | {
86 | "type": "object",
87 | "required": [
88 | "$type"
89 | ],
90 | "properties": {
91 | "$type": {
92 | "type": "string",
93 | "enum": [
94 | "closed"
95 | ]
96 | }
97 | }
98 | }
99 | ]
100 | },
101 | "Response": {
102 | "description": "Responses from the webview to the client.",
103 | "oneOf": [
104 | {
105 | "type": "object",
106 | "required": [
107 | "$type",
108 | "id"
109 | ],
110 | "properties": {
111 | "$type": {
112 | "type": "string",
113 | "enum": [
114 | "ack"
115 | ]
116 | },
117 | "id": {
118 | "type": "integer",
119 | "format": "int64"
120 | }
121 | }
122 | },
123 | {
124 | "type": "object",
125 | "required": [
126 | "$type",
127 | "id",
128 | "result"
129 | ],
130 | "properties": {
131 | "$type": {
132 | "type": "string",
133 | "enum": [
134 | "result"
135 | ]
136 | },
137 | "id": {
138 | "type": "integer",
139 | "format": "int64"
140 | },
141 | "result": {
142 | "$ref": "#/definitions/ResultType"
143 | }
144 | }
145 | },
146 | {
147 | "type": "object",
148 | "required": [
149 | "$type",
150 | "id",
151 | "message"
152 | ],
153 | "properties": {
154 | "$type": {
155 | "type": "string",
156 | "enum": [
157 | "err"
158 | ]
159 | },
160 | "id": {
161 | "type": "integer",
162 | "format": "int64"
163 | },
164 | "message": {
165 | "type": "string"
166 | }
167 | }
168 | }
169 | ]
170 | },
171 | "ResultType": {
172 | "description": "Types that can be returned from webview results.",
173 | "oneOf": [
174 | {
175 | "type": "object",
176 | "required": [
177 | "$type",
178 | "value"
179 | ],
180 | "properties": {
181 | "$type": {
182 | "type": "string",
183 | "enum": [
184 | "string"
185 | ]
186 | },
187 | "value": {
188 | "type": "string"
189 | }
190 | }
191 | },
192 | {
193 | "type": "object",
194 | "required": [
195 | "$type",
196 | "value"
197 | ],
198 | "properties": {
199 | "$type": {
200 | "type": "string",
201 | "enum": [
202 | "boolean"
203 | ]
204 | },
205 | "value": {
206 | "type": "boolean"
207 | }
208 | }
209 | },
210 | {
211 | "type": "object",
212 | "required": [
213 | "$type",
214 | "value"
215 | ],
216 | "properties": {
217 | "$type": {
218 | "type": "string",
219 | "enum": [
220 | "float"
221 | ]
222 | },
223 | "value": {
224 | "type": "number",
225 | "format": "double"
226 | }
227 | }
228 | },
229 | {
230 | "type": "object",
231 | "required": [
232 | "$type",
233 | "value"
234 | ],
235 | "properties": {
236 | "$type": {
237 | "type": "string",
238 | "enum": [
239 | "size"
240 | ]
241 | },
242 | "value": {
243 | "$ref": "#/definitions/SizeWithScale"
244 | }
245 | }
246 | }
247 | ]
248 | },
249 | "SizeWithScale": {
250 | "type": "object",
251 | "required": [
252 | "height",
253 | "scaleFactor",
254 | "width"
255 | ],
256 | "properties": {
257 | "height": {
258 | "description": "The height of the window in logical pixels.",
259 | "type": "number",
260 | "format": "double"
261 | },
262 | "scaleFactor": {
263 | "description": "The ratio between physical and logical sizes.",
264 | "type": "number",
265 | "format": "double"
266 | },
267 | "width": {
268 | "description": "The width of the window in logical pixels.",
269 | "type": "number",
270 | "format": "double"
271 | }
272 | }
273 | }
274 | }
275 | }
--------------------------------------------------------------------------------
/schemas/WebViewOptions.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "title": "Options",
4 | "description": "Options for creating a webview.",
5 | "type": "object",
6 | "required": [
7 | "title"
8 | ],
9 | "properties": {
10 | "acceptFirstMouse": {
11 | "description": "Sets whether clicking an inactive window also clicks through to the webview. Default is false.",
12 | "default": false,
13 | "type": "boolean"
14 | },
15 | "autoplay": {
16 | "description": "When true, all media can be played without user interaction. Default is false.",
17 | "default": false,
18 | "type": "boolean"
19 | },
20 | "clipboard": {
21 | "description": "Enables clipboard access for the page rendered on Linux and Windows.\n\nmacOS doesn’t provide such method and is always enabled by default. But your app will still need to add menu item accelerators to use the clipboard shortcuts.",
22 | "default": false,
23 | "type": "boolean"
24 | },
25 | "decorations": {
26 | "description": "When true, the window will have a border, a title bar, etc. Default is true.",
27 | "default": true,
28 | "type": "boolean"
29 | },
30 | "devtools": {
31 | "description": "Enable or disable webview devtools.\n\nNote this only enables devtools to the webview. To open it, you can call `webview.open_devtools()`, or right click the page and open it from the context menu.",
32 | "default": false,
33 | "type": "boolean"
34 | },
35 | "focused": {
36 | "description": "Sets whether the webview should be focused when created. Default is false.",
37 | "default": false,
38 | "type": "boolean"
39 | },
40 | "incognito": {
41 | "description": "Run the WebView with incognito mode. Note that WebContext will be ingored if incognito is enabled.\n\nPlatform-specific: - Windows: Requires WebView2 Runtime version 101.0.1210.39 or higher, does nothing on older versions, see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/archive?tabs=dotnetcsharp#10121039",
42 | "default": false,
43 | "type": "boolean"
44 | },
45 | "initializationScript": {
46 | "description": "Run JavaScript code when loading new pages. When the webview loads a new page, this code will be executed. It is guaranteed that the code is executed before window.onload.",
47 | "default": null,
48 | "type": [
49 | "string",
50 | "null"
51 | ]
52 | },
53 | "ipc": {
54 | "description": "Sets whether host should be able to receive messages from the webview via `window.ipc.postMessage`.",
55 | "default": false,
56 | "type": "boolean"
57 | },
58 | "load": {
59 | "description": "The content to load into the webview.",
60 | "anyOf": [
61 | {
62 | "$ref": "#/definitions/Content"
63 | },
64 | {
65 | "type": "null"
66 | }
67 | ]
68 | },
69 | "size": {
70 | "description": "The size of the window.",
71 | "anyOf": [
72 | {
73 | "$ref": "#/definitions/WindowSize"
74 | },
75 | {
76 | "type": "null"
77 | }
78 | ]
79 | },
80 | "title": {
81 | "description": "Sets the title of the window.",
82 | "type": "string"
83 | },
84 | "transparent": {
85 | "description": "Sets whether the window should be transparent.",
86 | "default": false,
87 | "type": "boolean"
88 | },
89 | "userAgent": {
90 | "description": "Sets the user agent to use when loading pages.",
91 | "default": null,
92 | "type": [
93 | "string",
94 | "null"
95 | ]
96 | }
97 | },
98 | "definitions": {
99 | "Content": {
100 | "description": "The content to load into the webview.",
101 | "anyOf": [
102 | {
103 | "type": "object",
104 | "required": [
105 | "url"
106 | ],
107 | "properties": {
108 | "headers": {
109 | "description": "Optional headers to send with the request.",
110 | "type": [
111 | "object",
112 | "null"
113 | ],
114 | "additionalProperties": {
115 | "type": "string"
116 | }
117 | },
118 | "url": {
119 | "description": "Url to load in the webview. Note: Don't use data URLs here, as they are not supported. Use the `html` field instead.",
120 | "type": "string"
121 | }
122 | }
123 | },
124 | {
125 | "type": "object",
126 | "required": [
127 | "html"
128 | ],
129 | "properties": {
130 | "html": {
131 | "description": "Html to load in the webview.",
132 | "type": "string"
133 | },
134 | "origin": {
135 | "description": "What to set as the origin of the webview when loading html.",
136 | "default": "init",
137 | "type": "string"
138 | }
139 | }
140 | }
141 | ]
142 | },
143 | "Size": {
144 | "type": "object",
145 | "required": [
146 | "height",
147 | "width"
148 | ],
149 | "properties": {
150 | "height": {
151 | "description": "The height of the window in logical pixels.",
152 | "type": "number",
153 | "format": "double"
154 | },
155 | "width": {
156 | "description": "The width of the window in logical pixels.",
157 | "type": "number",
158 | "format": "double"
159 | }
160 | }
161 | },
162 | "WindowSize": {
163 | "anyOf": [
164 | {
165 | "$ref": "#/definitions/WindowSizeStates"
166 | },
167 | {
168 | "$ref": "#/definitions/Size"
169 | }
170 | ]
171 | },
172 | "WindowSizeStates": {
173 | "type": "string",
174 | "enum": [
175 | "maximized",
176 | "fullscreen"
177 | ]
178 | }
179 | }
180 | }
--------------------------------------------------------------------------------
/schemas/WebViewRequest.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "title": "Request",
4 | "description": "Explicit requests from the client to the webview.",
5 | "oneOf": [
6 | {
7 | "type": "object",
8 | "required": [
9 | "$type",
10 | "id"
11 | ],
12 | "properties": {
13 | "$type": {
14 | "type": "string",
15 | "enum": [
16 | "getVersion"
17 | ]
18 | },
19 | "id": {
20 | "description": "The id of the request.",
21 | "type": "integer",
22 | "format": "int64"
23 | }
24 | }
25 | },
26 | {
27 | "type": "object",
28 | "required": [
29 | "$type",
30 | "id",
31 | "js"
32 | ],
33 | "properties": {
34 | "$type": {
35 | "type": "string",
36 | "enum": [
37 | "eval"
38 | ]
39 | },
40 | "id": {
41 | "description": "The id of the request.",
42 | "type": "integer",
43 | "format": "int64"
44 | },
45 | "js": {
46 | "description": "The javascript to evaluate.",
47 | "type": "string"
48 | }
49 | }
50 | },
51 | {
52 | "type": "object",
53 | "required": [
54 | "$type",
55 | "id",
56 | "title"
57 | ],
58 | "properties": {
59 | "$type": {
60 | "type": "string",
61 | "enum": [
62 | "setTitle"
63 | ]
64 | },
65 | "id": {
66 | "description": "The id of the request.",
67 | "type": "integer",
68 | "format": "int64"
69 | },
70 | "title": {
71 | "description": "The title to set.",
72 | "type": "string"
73 | }
74 | }
75 | },
76 | {
77 | "type": "object",
78 | "required": [
79 | "$type",
80 | "id"
81 | ],
82 | "properties": {
83 | "$type": {
84 | "type": "string",
85 | "enum": [
86 | "getTitle"
87 | ]
88 | },
89 | "id": {
90 | "description": "The id of the request.",
91 | "type": "integer",
92 | "format": "int64"
93 | }
94 | }
95 | },
96 | {
97 | "type": "object",
98 | "required": [
99 | "$type",
100 | "id",
101 | "visible"
102 | ],
103 | "properties": {
104 | "$type": {
105 | "type": "string",
106 | "enum": [
107 | "setVisibility"
108 | ]
109 | },
110 | "id": {
111 | "description": "The id of the request.",
112 | "type": "integer",
113 | "format": "int64"
114 | },
115 | "visible": {
116 | "description": "Whether the window should be visible or hidden.",
117 | "type": "boolean"
118 | }
119 | }
120 | },
121 | {
122 | "type": "object",
123 | "required": [
124 | "$type",
125 | "id"
126 | ],
127 | "properties": {
128 | "$type": {
129 | "type": "string",
130 | "enum": [
131 | "isVisible"
132 | ]
133 | },
134 | "id": {
135 | "description": "The id of the request.",
136 | "type": "integer",
137 | "format": "int64"
138 | }
139 | }
140 | },
141 | {
142 | "type": "object",
143 | "required": [
144 | "$type",
145 | "id"
146 | ],
147 | "properties": {
148 | "$type": {
149 | "type": "string",
150 | "enum": [
151 | "openDevTools"
152 | ]
153 | },
154 | "id": {
155 | "description": "The id of the request.",
156 | "type": "integer",
157 | "format": "int64"
158 | }
159 | }
160 | },
161 | {
162 | "type": "object",
163 | "required": [
164 | "$type",
165 | "id"
166 | ],
167 | "properties": {
168 | "$type": {
169 | "type": "string",
170 | "enum": [
171 | "getSize"
172 | ]
173 | },
174 | "id": {
175 | "description": "The id of the request.",
176 | "type": "integer",
177 | "format": "int64"
178 | },
179 | "include_decorations": {
180 | "description": "Whether to include the title bar and borders in the size measurement.",
181 | "default": null,
182 | "type": [
183 | "boolean",
184 | "null"
185 | ]
186 | }
187 | }
188 | },
189 | {
190 | "type": "object",
191 | "required": [
192 | "$type",
193 | "id",
194 | "size"
195 | ],
196 | "properties": {
197 | "$type": {
198 | "type": "string",
199 | "enum": [
200 | "setSize"
201 | ]
202 | },
203 | "id": {
204 | "description": "The id of the request.",
205 | "type": "integer",
206 | "format": "int64"
207 | },
208 | "size": {
209 | "description": "The size to set.",
210 | "allOf": [
211 | {
212 | "$ref": "#/definitions/Size"
213 | }
214 | ]
215 | }
216 | }
217 | },
218 | {
219 | "type": "object",
220 | "required": [
221 | "$type",
222 | "id"
223 | ],
224 | "properties": {
225 | "$type": {
226 | "type": "string",
227 | "enum": [
228 | "fullscreen"
229 | ]
230 | },
231 | "fullscreen": {
232 | "description": "Whether to enter fullscreen mode. If left unspecified, the window will enter fullscreen mode if it is not already in fullscreen mode or exit fullscreen mode if it is currently in fullscreen mode.",
233 | "type": [
234 | "boolean",
235 | "null"
236 | ]
237 | },
238 | "id": {
239 | "description": "The id of the request.",
240 | "type": "integer",
241 | "format": "int64"
242 | }
243 | }
244 | },
245 | {
246 | "type": "object",
247 | "required": [
248 | "$type",
249 | "id"
250 | ],
251 | "properties": {
252 | "$type": {
253 | "type": "string",
254 | "enum": [
255 | "maximize"
256 | ]
257 | },
258 | "id": {
259 | "description": "The id of the request.",
260 | "type": "integer",
261 | "format": "int64"
262 | },
263 | "maximized": {
264 | "description": "Whether to maximize the window. If left unspecified, the window will be maximized if it is not already maximized or restored if it was previously maximized.",
265 | "type": [
266 | "boolean",
267 | "null"
268 | ]
269 | }
270 | }
271 | },
272 | {
273 | "type": "object",
274 | "required": [
275 | "$type",
276 | "id"
277 | ],
278 | "properties": {
279 | "$type": {
280 | "type": "string",
281 | "enum": [
282 | "minimize"
283 | ]
284 | },
285 | "id": {
286 | "description": "The id of the request.",
287 | "type": "integer",
288 | "format": "int64"
289 | },
290 | "minimized": {
291 | "description": "Whether to minimize the window. If left unspecified, the window will be minimized if it is not already minimized or restored if it was previously minimized.",
292 | "type": [
293 | "boolean",
294 | "null"
295 | ]
296 | }
297 | }
298 | },
299 | {
300 | "type": "object",
301 | "required": [
302 | "$type",
303 | "html",
304 | "id"
305 | ],
306 | "properties": {
307 | "$type": {
308 | "type": "string",
309 | "enum": [
310 | "loadHtml"
311 | ]
312 | },
313 | "html": {
314 | "description": "HTML to set as the content of the webview.",
315 | "type": "string"
316 | },
317 | "id": {
318 | "description": "The id of the request.",
319 | "type": "integer",
320 | "format": "int64"
321 | },
322 | "origin": {
323 | "description": "What to set as the origin of the webview when loading html. If not specified, the origin will be set to the value of the `origin` field when the webview was created.",
324 | "type": [
325 | "string",
326 | "null"
327 | ]
328 | }
329 | }
330 | },
331 | {
332 | "type": "object",
333 | "required": [
334 | "$type",
335 | "id",
336 | "url"
337 | ],
338 | "properties": {
339 | "$type": {
340 | "type": "string",
341 | "enum": [
342 | "loadUrl"
343 | ]
344 | },
345 | "headers": {
346 | "description": "Optional headers to send with the request.",
347 | "type": [
348 | "object",
349 | "null"
350 | ],
351 | "additionalProperties": {
352 | "type": "string"
353 | }
354 | },
355 | "id": {
356 | "description": "The id of the request.",
357 | "type": "integer",
358 | "format": "int64"
359 | },
360 | "url": {
361 | "description": "URL to load in the webview.",
362 | "type": "string"
363 | }
364 | }
365 | }
366 | ],
367 | "definitions": {
368 | "Size": {
369 | "type": "object",
370 | "required": [
371 | "height",
372 | "width"
373 | ],
374 | "properties": {
375 | "height": {
376 | "description": "The height of the window in logical pixels.",
377 | "type": "number",
378 | "format": "double"
379 | },
380 | "width": {
381 | "description": "The width of the window in logical pixels.",
382 | "type": "number",
383 | "format": "double"
384 | }
385 | }
386 | }
387 | }
388 | }
--------------------------------------------------------------------------------
/schemas/WebViewResponse.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "title": "Response",
4 | "description": "Responses from the webview to the client.",
5 | "oneOf": [
6 | {
7 | "type": "object",
8 | "required": [
9 | "$type",
10 | "id"
11 | ],
12 | "properties": {
13 | "$type": {
14 | "type": "string",
15 | "enum": [
16 | "ack"
17 | ]
18 | },
19 | "id": {
20 | "type": "integer",
21 | "format": "int64"
22 | }
23 | }
24 | },
25 | {
26 | "type": "object",
27 | "required": [
28 | "$type",
29 | "id",
30 | "result"
31 | ],
32 | "properties": {
33 | "$type": {
34 | "type": "string",
35 | "enum": [
36 | "result"
37 | ]
38 | },
39 | "id": {
40 | "type": "integer",
41 | "format": "int64"
42 | },
43 | "result": {
44 | "$ref": "#/definitions/ResultType"
45 | }
46 | }
47 | },
48 | {
49 | "type": "object",
50 | "required": [
51 | "$type",
52 | "id",
53 | "message"
54 | ],
55 | "properties": {
56 | "$type": {
57 | "type": "string",
58 | "enum": [
59 | "err"
60 | ]
61 | },
62 | "id": {
63 | "type": "integer",
64 | "format": "int64"
65 | },
66 | "message": {
67 | "type": "string"
68 | }
69 | }
70 | }
71 | ],
72 | "definitions": {
73 | "ResultType": {
74 | "description": "Types that can be returned from webview results.",
75 | "oneOf": [
76 | {
77 | "type": "object",
78 | "required": [
79 | "$type",
80 | "value"
81 | ],
82 | "properties": {
83 | "$type": {
84 | "type": "string",
85 | "enum": [
86 | "string"
87 | ]
88 | },
89 | "value": {
90 | "type": "string"
91 | }
92 | }
93 | },
94 | {
95 | "type": "object",
96 | "required": [
97 | "$type",
98 | "value"
99 | ],
100 | "properties": {
101 | "$type": {
102 | "type": "string",
103 | "enum": [
104 | "boolean"
105 | ]
106 | },
107 | "value": {
108 | "type": "boolean"
109 | }
110 | }
111 | },
112 | {
113 | "type": "object",
114 | "required": [
115 | "$type",
116 | "value"
117 | ],
118 | "properties": {
119 | "$type": {
120 | "type": "string",
121 | "enum": [
122 | "float"
123 | ]
124 | },
125 | "value": {
126 | "type": "number",
127 | "format": "double"
128 | }
129 | }
130 | },
131 | {
132 | "type": "object",
133 | "required": [
134 | "$type",
135 | "value"
136 | ],
137 | "properties": {
138 | "$type": {
139 | "type": "string",
140 | "enum": [
141 | "size"
142 | ]
143 | },
144 | "value": {
145 | "$ref": "#/definitions/SizeWithScale"
146 | }
147 | }
148 | }
149 | ]
150 | },
151 | "SizeWithScale": {
152 | "type": "object",
153 | "required": [
154 | "height",
155 | "scaleFactor",
156 | "width"
157 | ],
158 | "properties": {
159 | "height": {
160 | "description": "The height of the window in logical pixels.",
161 | "type": "number",
162 | "format": "double"
163 | },
164 | "scaleFactor": {
165 | "description": "The ratio between physical and logical sizes.",
166 | "type": "number",
167 | "format": "double"
168 | },
169 | "width": {
170 | "description": "The width of the window in logical pixels.",
171 | "type": "number",
172 | "format": "double"
173 | }
174 | }
175 | }
176 | }
177 | }
--------------------------------------------------------------------------------
/scripts/generate-schema/debug.ts:
--------------------------------------------------------------------------------
1 | import { parseSchema } from "./parser.ts";
2 | import { printDocIR } from "./printer.ts";
3 |
4 | const schemaFiles = [
5 | "WebViewOptions.json",
6 | "WebViewRequest.json",
7 | "WebViewResponse.json",
8 | "WebViewMessage.json",
9 | ];
10 |
11 | async function main() {
12 | const targetSchema = Deno.args[0];
13 | const filesToProcess = targetSchema
14 | ? [targetSchema.endsWith(".json") ? targetSchema : `${targetSchema}.json`]
15 | : schemaFiles;
16 |
17 | for (const schemaFile of filesToProcess) {
18 | if (!schemaFiles.includes(schemaFile)) {
19 | console.error(`Invalid schema file: ${schemaFile}`);
20 | console.error(`Available schemas: ${schemaFiles.join(", ")}`);
21 | Deno.exit(1);
22 | }
23 |
24 | console.log(`Schema: ${schemaFile}`);
25 |
26 | const schema = JSON.parse(await Deno.readTextFile(`schemas/${schemaFile}`));
27 | const doc = parseSchema(schema);
28 | console.log(printDocIR(doc));
29 | }
30 | }
31 |
32 | if (import.meta.main) {
33 | main().catch(console.error);
34 | }
35 |
--------------------------------------------------------------------------------
/scripts/generate-schema/gen-helpers.ts:
--------------------------------------------------------------------------------
1 | export class Writer {
2 | constructor(private buffer: string = "") {}
3 |
4 | append(...t: (string | false | undefined | null | 0)[]) {
5 | this.buffer += t.filter((t) => t).join(" ");
6 | }
7 |
8 | appendLine(...t: (string | false | undefined | null | 0)[]) {
9 | this.append(...t, "\n");
10 | }
11 |
12 | prepend(...t: (string | false | undefined | null | 0)[]) {
13 | this.buffer = t.filter((t) => t).join(" ") + this.buffer;
14 | }
15 |
16 | shorthand() {
17 | return {
18 | w: this.append.bind(this),
19 | wn: this.appendLine.bind(this),
20 | };
21 | }
22 |
23 | output() {
24 | return this.buffer;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/scripts/generate-schema/gen-python.test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals } from "jsr:@std/assert";
2 | import dedent from "npm:dedent";
3 | import { extractExportedNames } from "./gen-python.ts";
4 |
5 | Deno.test("extractExportedNames - extracts class names", () => {
6 | const content = dedent`
7 | class MyClass:
8 | pass
9 |
10 | class AnotherClass(msgspec.Struct):
11 | field: str
12 |
13 | def not_a_class():
14 | pass
15 | `;
16 | assertEquals(extractExportedNames(content), ["AnotherClass", "MyClass"]);
17 | });
18 |
19 | Deno.test("extractExportedNames - extracts enum assignments", () => {
20 | const content = dedent`
21 | MyEnum = Union[ClassA, ClassB]
22 | AnotherEnum = Union[ClassC, ClassD, ClassE]
23 |
24 | not_an_enum = "something else"
25 | `;
26 | assertEquals(extractExportedNames(content), ["AnotherEnum", "MyEnum"]);
27 | });
28 |
29 | Deno.test("extractExportedNames - extracts both classes and enums", () => {
30 | const content = dedent`
31 | class MyClass:
32 | pass
33 |
34 | MyEnum = Union[ClassA, ClassB]
35 |
36 | class AnotherClass:
37 | field: str
38 |
39 | AnotherEnum = Union[ClassC, ClassD]
40 | `;
41 | assertEquals(extractExportedNames(content), [
42 | "AnotherClass",
43 | "AnotherEnum",
44 | "MyClass",
45 | "MyEnum",
46 | ]);
47 | });
48 |
49 | Deno.test("extractExportedNames - handles empty content", () => {
50 | assertEquals(extractExportedNames(""), []);
51 | });
52 |
53 | Deno.test("extractExportedNames - ignores indented class definitions and enum assignments", () => {
54 | const content = dedent`
55 | def some_function():
56 | class IndentedClass:
57 | pass
58 |
59 | IndentedEnum = Union[ClassA, ClassB]
60 |
61 | class TopLevelClass:
62 | pass
63 |
64 | TopLevelEnum = Union[ClassC, ClassD]
65 | `;
66 | assertEquals(extractExportedNames(content), [
67 | "TopLevelClass",
68 | "TopLevelEnum",
69 | ]);
70 | });
71 |
--------------------------------------------------------------------------------
/scripts/generate-schema/gen-python.ts:
--------------------------------------------------------------------------------
1 | import { type Doc, isComplexType, type Node } from "./parser.ts";
2 | import { match, P } from "npm:ts-pattern";
3 | import { Writer } from "./gen-helpers.ts";
4 | import { assert } from "jsr:@std/assert";
5 |
6 | const header = (relativePath: string) =>
7 | `# DO NOT EDIT: This file is auto-generated by ${relativePath}\n` +
8 | "from enum import Enum\n" +
9 | "from typing import Any, Literal, Optional, Union\n" +
10 | "import msgspec\n\n";
11 |
12 | export function extractExportedNames(content: string): string[] {
13 | const names = new Set();
14 |
15 | // Match class definitions and enum assignments
16 | const classRegex = /^class\s+([a-zA-Z_][a-zA-Z0-9_]*)/gm;
17 | const enumRegex = /^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*Union\[/gm;
18 |
19 | let match;
20 | while ((match = classRegex.exec(content)) !== null) {
21 | names.add(match[1]);
22 | }
23 | while ((match = enumRegex.exec(content)) !== null) {
24 | names.add(match[1]);
25 | }
26 |
27 | return [...names].sort();
28 | }
29 |
30 | export function generateAll(names: string[]): string {
31 | return `__all__ = ${JSON.stringify(names, null, 4)}\n\n`;
32 | }
33 |
34 | // Track generated definitions to avoid duplicates
35 | const generatedDefinitions = new Set();
36 | const generatedDependentClasses = new Set();
37 |
38 | export function generatePython(
39 | doc: Doc,
40 | name: string,
41 | relativePath: string,
42 | ): string {
43 | // Only include header for the first schema
44 | const shouldIncludeHeader = generatedDefinitions.size === 0;
45 | const content = generateTypes(doc, name);
46 |
47 | let output = "";
48 | if (shouldIncludeHeader) {
49 | output += header(relativePath);
50 | }
51 | return output + content;
52 | }
53 |
54 | function generateTypes(
55 | doc: Doc,
56 | name: string,
57 | ) {
58 | const writer = new Writer();
59 |
60 | let definitions = "";
61 | const skipAssignments = ["object", "intersection", "enum", "union"];
62 | for (const [defName, definition] of Object.entries(doc.definitions)) {
63 | // Skip if we've already generated this definition
64 | if (generatedDefinitions.has(defName)) {
65 | continue;
66 | }
67 | generatedDefinitions.add(defName);
68 |
69 | const definitionWriter = new Writer();
70 | const { w, wn } = definitionWriter.shorthand();
71 | if (!skipAssignments.includes(definition.type)) {
72 | w(defName, " = ");
73 | }
74 | generateNode(definition, definitionWriter);
75 | if (definition.description) {
76 | wn('"""');
77 | wn(`${definition.description}`);
78 | wn('"""');
79 | }
80 | definitions += definitionWriter.output();
81 | }
82 |
83 | const { w, wn } = writer.shorthand();
84 |
85 | if (!generatedDefinitions.has(name)) {
86 | if (!skipAssignments.includes(doc.root.type)) {
87 | w(name, " = ");
88 | }
89 | generateNode(doc.root, writer);
90 | if (doc.description && doc.root.type !== "object") {
91 | wn('"""');
92 | wn(`${doc.description}`);
93 | wn('"""');
94 | }
95 | }
96 |
97 | return definitions + writer.output();
98 | }
99 |
100 | function sortByRequired(
101 | properties: T[],
102 | ): T[] {
103 | return [...properties].sort((a, b) => {
104 | if (a.required === b.required) return 0;
105 | return a.required ? -1 : 1;
106 | });
107 | }
108 |
109 | function generateNode(node: Node, writer: Writer) {
110 | const { w, wn } = writer.shorthand();
111 | using context = new Context(node);
112 | match(node)
113 | .with({ type: "reference" }, ({ name }) => w(name))
114 | .with({ type: "int" }, () => w("int"))
115 | .with({ type: "float" }, () => w("float"))
116 | .with({ type: "boolean" }, () => w("bool"))
117 | .with({ type: "string" }, () => w("str"))
118 | .with({ type: "literal" }, (node) => w(`Literal["${node.value}"]`))
119 | .with(
120 | { type: "record" },
121 | (node) => w(`dict[str, ${mapPythonType(node.valueType)}]`),
122 | )
123 | .with({ type: "enum" }, (node) => {
124 | wn(`class ${node.name}(str, Enum):`);
125 | for (const value of node.members) {
126 | wn(` ${value} = "${value}"`);
127 | }
128 | wn("");
129 | })
130 | .with({ type: "union" }, (node) => {
131 | const depWriter = new Writer();
132 | const classes = node.members.map((m) => {
133 | let name: string = "";
134 | if (m.name) {
135 | name = m.name;
136 | } else {
137 | const ident = m.type === "object"
138 | ? m.properties?.find((p) => p.required)?.key ?? ""
139 | : "";
140 | name = `${node.name}${cap(ident)}`;
141 | }
142 | if (!generatedDependentClasses.has(name)) {
143 | generatedDependentClasses.add(name);
144 | if (isComplexType(m)) {
145 | generateNode(m, depWriter);
146 | }
147 | }
148 | return name;
149 | });
150 | writer.append(depWriter.output());
151 | wn(`${node.name} = Union[${classes.join(", ")}]`);
152 | })
153 | .with({ type: "object" }, (node) => {
154 | match(context.parent)
155 | .with({ type: "union" }, () => {
156 | const name = context.closestName();
157 | const ident = node.properties.find((p) => p.required)?.key ?? "";
158 | wn(
159 | `class ${name}${
160 | cap(ident)
161 | }(msgspec.Struct, kw_only=True, omit_defaults=True):`,
162 | );
163 | })
164 | .with(P.nullish, () => {
165 | wn(`class ${node.name}(msgspec.Struct, omit_defaults=True):`);
166 | })
167 | .otherwise(() => {
168 | wn(
169 | `class ${node.name}(msgspec.Struct, kw_only=True, omit_defaults=True):`,
170 | );
171 | });
172 | if (node.description) {
173 | wn(` """`);
174 | wn(` ${node.description}`);
175 | wn(` """`);
176 | }
177 |
178 | const sortedProperties = sortByRequired(node.properties);
179 |
180 | for (const { key, required, description, value } of sortedProperties) {
181 | w(` ${key}: `);
182 | if (!required) w("Union[");
183 | generateNode(value, writer);
184 | if (!required) w(", None] = None");
185 | wn("");
186 | if (description) {
187 | wn(` """${description}"""`);
188 | }
189 | }
190 | wn("");
191 | })
192 | .with({ type: "descriminated-union" }, (node) => {
193 | const depWriter = new Writer();
194 | const { w: d, wn: dn } = depWriter.shorthand();
195 | const classes: string[] = [];
196 | w("Union[");
197 | for (const [name, properties] of Object.entries(node.members)) {
198 | for (const { value } of properties) {
199 | if (isComplexType(value)) {
200 | generateNode(value, depWriter);
201 | }
202 | }
203 | const className = `${cap(name)}${cap(node.name!)}`;
204 | classes.push(className);
205 | if (!generatedDependentClasses.has(className)) {
206 | generatedDependentClasses.add(className);
207 | dn(
208 | `class ${className}(msgspec.Struct, tag_field="${node.descriminator}", tag="${name}"):`,
209 | );
210 | if (properties.length === 0) {
211 | dn(" pass");
212 | }
213 |
214 | const sortedProperties = sortByRequired(properties);
215 |
216 | for (
217 | const { key, required, description, value } of sortedProperties
218 | ) {
219 | d(` ${key}: `);
220 | if (!required) d("Union[");
221 | !isComplexType(value)
222 | ? generateNode(value, depWriter)
223 | : d(value.name ?? value.type);
224 | if (!required) d(", None] = None");
225 | dn("");
226 | if (description) {
227 | dn(` """${description}"""`);
228 | }
229 | }
230 | dn("");
231 | }
232 | }
233 | w(classes.join(", "));
234 | writer.prepend(depWriter.output());
235 | wn("]");
236 | })
237 | .with({ type: "intersection" }, (node) => {
238 | assert(
239 | node.members.length === 2,
240 | "Intersection must have exactly 2 members",
241 | );
242 | assert(
243 | node.members[0]?.type === "object",
244 | "First member of intersection must be an object",
245 | );
246 | for (const member of node.members) {
247 | generateNode(member, writer);
248 | }
249 | })
250 | .with({ type: "unknown" }, () => {
251 | w("Any");
252 | })
253 | .exhaustive();
254 | }
255 |
256 | class Context {
257 | private static stack: Node[] = [];
258 | constructor(public readonly currentNode: Node) {
259 | Context.stack.push(this.currentNode);
260 | }
261 |
262 | get parent(): Node | undefined {
263 | return Context.stack.at(-2);
264 | }
265 |
266 | /**
267 | * When `n` is 1, this returns the parent.
268 | * When `n` is 2, this returns the grandparent.
269 | * etc.
270 | */
271 | getNthParent(n: number): Node | undefined {
272 | return Context.stack.at(-(n + 1));
273 | }
274 |
275 | closestName(): string | undefined {
276 | for (const node of [...Context.stack].reverse()) {
277 | // @ts-expect-error - We're looking for names on roots or declarations, this should be fine.
278 | const name = node.name || node.title;
279 | if (name) {
280 | return name;
281 | }
282 | }
283 | return undefined;
284 | }
285 |
286 | [Symbol.dispose]() {
287 | Context.stack = Context.stack.filter((n) => n !== this.currentNode);
288 | }
289 | }
290 |
291 | function cap(str: string): string {
292 | if (!str) return "";
293 | return str.charAt(0).toUpperCase() + str.slice(1);
294 | }
295 |
296 | function mapPythonType(type: string): string {
297 | return match(type)
298 | .with("string", () => "str")
299 | .with("number", () => "float")
300 | .with("integer", () => "int")
301 | .with("boolean", () => "bool")
302 | .otherwise(() => "Any");
303 | }
304 |
--------------------------------------------------------------------------------
/scripts/generate-schema/gen-typescript.ts:
--------------------------------------------------------------------------------
1 | import type { Doc, Node } from "./parser.ts";
2 | import { Writer } from "./gen-helpers.ts";
3 | import { match } from "npm:ts-pattern";
4 |
5 | const header = (relativePath: string) =>
6 | `// DO NOT EDIT: This file is auto-generated by ${relativePath}\n` +
7 | "import { z } from 'npm:zod';\n\n";
8 |
9 | // Track generated definitions to avoid duplicates
10 | const generatedTypeDefinitions = new Set();
11 | const generatedZodDefinitions = new Set();
12 |
13 | export function generateTypeScript(
14 | doc: Doc,
15 | name: string,
16 | relativePath: string,
17 | ) {
18 | // Only include header for the first schema
19 | const shouldIncludeHeader = generatedTypeDefinitions.size === 0;
20 | const types = generateTypes(doc, name);
21 | const zodSchema = generateZodSchema(doc, name);
22 | return (shouldIncludeHeader ? header(relativePath) : "") + types + zodSchema;
23 | }
24 |
25 | function generateTypes(doc: Doc, typeName: string) {
26 | const writer = new Writer();
27 | const { w, wn } = writer.shorthand();
28 |
29 | for (const [name, definition] of Object.entries(doc.definitions)) {
30 | // Skip if we've already generated this definition
31 | if (generatedTypeDefinitions.has(name)) {
32 | continue;
33 | }
34 | generatedTypeDefinitions.add(name);
35 |
36 | if (definition.description) {
37 | wn("/**");
38 | wn(` * ${definition.description}`);
39 | wn(" */");
40 | }
41 | wn("export type", name, " = ");
42 | generateNode(definition);
43 | wn("");
44 | }
45 |
46 | if (!generatedTypeDefinitions.has(typeName)) {
47 | if (doc.description) {
48 | wn("/**");
49 | wn(` * ${doc.description}`);
50 | wn(" */");
51 | }
52 | wn("export type", typeName, " = ");
53 | generateNode(doc.root);
54 | wn("");
55 | }
56 |
57 | function generateNode(node: Node) {
58 | match(node)
59 | .with({ type: "reference" }, (node) => w(node.name))
60 | .with({ type: "int" }, () => w("number"))
61 | .with({ type: "float" }, () => w("number"))
62 | .with({ type: "boolean" }, () => w("boolean"))
63 | .with({ type: "string" }, () => w("string"))
64 | .with({ type: "literal" }, (node) => w(`"${node.value}"`))
65 | .with(
66 | { type: "record" },
67 | (node) => w(`Record`),
68 | )
69 | .with({ type: "object" }, (node) => {
70 | wn("{");
71 | for (const { key, required, description, value } of node.properties) {
72 | if (description) {
73 | if (description.includes("\n")) {
74 | wn(`/**`);
75 | for (const line of description.split("\n")) {
76 | wn(` * ${line}`);
77 | }
78 | wn(` */`);
79 | } else {
80 | wn(`/** ${description} */`);
81 | }
82 | }
83 | w(key, required ? ": " : "? : ");
84 | generateNode(value);
85 | wn(",");
86 | }
87 | wn("}");
88 | })
89 | .with({ type: "intersection" }, (node) => {
90 | wn("(");
91 | for (const member of node.members) {
92 | w("& ");
93 | generateNode(member);
94 | }
95 | wn(")");
96 | })
97 | .with({ type: "union" }, (node) => {
98 | wn("(");
99 | for (const member of node.members) {
100 | generateNode(member);
101 | if (member !== node.members.at(-1)) {
102 | w(" | ");
103 | }
104 | }
105 | wn(")");
106 | })
107 | .with({ type: "descriminated-union" }, (node) => {
108 | wn("(");
109 | for (const [name, member] of Object.entries(node.members)) {
110 | wn("| {");
111 | wn(`${node.descriminator}: "${name}",`);
112 | for (const { key, required, description, value } of member) {
113 | wn(description ? `/** ${description} */` : "");
114 | w(key, required ? ": " : "? : ");
115 | generateNode(value);
116 | wn(",");
117 | }
118 | wn("}");
119 | }
120 | wn(")");
121 | })
122 | .with({ type: "enum" }, (node) => {
123 | w("(");
124 | w(node.members.map((m) => `"${m}"`).join(" | "));
125 | w(")");
126 | })
127 | .with({ type: "unknown" }, () => w("unknown"))
128 | .exhaustive();
129 | }
130 | return writer.output();
131 | }
132 |
133 | export function generateZodSchema(doc: Doc, name: string) {
134 | let result = "";
135 | const w = (...t: (string | false | undefined | null | 0)[]) => {
136 | result += t.filter((t) => t).join(" ");
137 | };
138 | const wn = (...t: (string | false | undefined | null | 0)[]) => w(...t, "\n");
139 |
140 | for (const [name, definition] of Object.entries(doc.definitions)) {
141 | // Skip if we've already generated this Zod definition
142 | if (generatedZodDefinitions.has(name)) {
143 | continue;
144 | }
145 | generatedZodDefinitions.add(name);
146 |
147 | wn("export const", name, `: z.ZodType<${name}> =`);
148 | generateNode(definition);
149 | wn("");
150 | }
151 |
152 | if (!generatedZodDefinitions.has(name)) {
153 | wn("export const", name, `: z.ZodType<${name}> =`);
154 | generateNode(doc.root);
155 | wn("");
156 | }
157 |
158 | function generateNode(node: Node) {
159 | match(node)
160 | .with({ type: "reference" }, (node) => w(node.name))
161 | .with({ type: "int" }, (node) => {
162 | w("z.number().int()");
163 | if (typeof node.minimum === "number") {
164 | w(`.min(${node.minimum})`);
165 | }
166 | if (typeof node.maximum === "number") {
167 | w(`.max(${node.maximum})`);
168 | }
169 | })
170 | .with({ type: "float" }, (node) => {
171 | w("z.number()");
172 | if (typeof node.minimum === "number") {
173 | w(`.min(${node.minimum})`);
174 | }
175 | if (typeof node.maximum === "number") {
176 | w(`.max(${node.maximum})`);
177 | }
178 | })
179 | .with(
180 | { type: "boolean" },
181 | (node) => w("z.boolean()", node.optional && ".optional()"),
182 | )
183 | .with({ type: "string" }, () => w("z.string()"))
184 | .with({ type: "literal" }, (node) => w(`z.literal("${node.value}")`))
185 | .with(
186 | { type: "record" },
187 | (node) => w(`z.record(z.string(), z.${node.valueType}())`),
188 | )
189 | .with({ type: "object" }, (node) => {
190 | w("z.object({");
191 | for (const { key, required, value } of node.properties) {
192 | w(key, ":");
193 | generateNode(value);
194 | if (!required && !("optional" in value && value.optional)) {
195 | w(".optional()");
196 | }
197 | w(",");
198 | }
199 | wn("})");
200 | })
201 | .with({ type: "descriminated-union" }, (node) => {
202 | w(`z.discriminatedUnion("${node.descriminator}", [`);
203 | for (const [name, member] of Object.entries(node.members)) {
204 | w(`z.object({ ${node.descriminator}: z.literal("${name}"),`);
205 | for (const { key, required, value } of member) {
206 | w(key, ": ");
207 | generateNode(value);
208 | if (!required) {
209 | w(".optional()");
210 | }
211 | wn(",");
212 | }
213 | wn("}),");
214 | }
215 | wn("])");
216 | })
217 | .with({ type: "intersection" }, (node) => {
218 | w("z.intersection(");
219 | for (const member of node.members) {
220 | generateNode(member);
221 | w(",");
222 | }
223 | wn(")");
224 | })
225 | .with({ type: "union" }, (node) => {
226 | w("z.union([");
227 | for (const member of node.members) {
228 | generateNode(member);
229 | w(",");
230 | }
231 | wn("])");
232 | })
233 | .with({ type: "enum" }, (node) => {
234 | w("z.enum([");
235 | w(node.members.map((m) => `"${m}"`).join(", "));
236 | w("])");
237 | })
238 | .with({ type: "unknown" }, () => w("z.unknown()"))
239 | .exhaustive();
240 | }
241 |
242 | return result;
243 | }
244 |
--------------------------------------------------------------------------------
/scripts/generate-schema/index.ts:
--------------------------------------------------------------------------------
1 | import { walk } from "jsr:@std/fs/walk";
2 | import { join } from "jsr:@std/path";
3 | import { parseArgs } from "jsr:@std/cli/parse-args";
4 | import type { JSONSchema } from "../../json-schema.d.ts";
5 | import { generateTypeScript } from "./gen-typescript.ts";
6 | import {
7 | extractExportedNames,
8 | generateAll,
9 | generatePython,
10 | } from "./gen-python.ts";
11 | import { parseSchema } from "./parser.ts";
12 |
13 | const schemasDir = new URL("../../schemas", import.meta.url).pathname;
14 | const tsSchemaDir = new URL("../../src/clients/deno", import.meta.url).pathname;
15 | const pySchemaDir =
16 | new URL("../../src/clients/python/src/justbe_webview", import.meta.url)
17 | .pathname;
18 |
19 | async function ensureDir(dir: string) {
20 | try {
21 | await Deno.mkdir(dir, { recursive: true });
22 | } catch (error) {
23 | if (!(error instanceof Deno.errors.AlreadyExists)) {
24 | throw error;
25 | }
26 | }
27 | }
28 |
29 | async function main() {
30 | const flags = parseArgs(Deno.args, {
31 | string: ["language"],
32 | alias: { language: "l" },
33 | });
34 |
35 | const language = flags.language?.toLowerCase();
36 | if (language && !["typescript", "python"].includes(language)) {
37 | console.error('Language must be either "typescript" or "python"');
38 | Deno.exit(1);
39 | }
40 |
41 | const relativePath = new URL(import.meta.url).pathname.split("/").slice(-2)
42 | .join("/");
43 |
44 | // Only ensure directories for the languages we'll generate
45 | if (!language || language === "typescript") {
46 | await ensureDir(tsSchemaDir);
47 | }
48 | if (!language || language === "python") {
49 | await ensureDir(pySchemaDir);
50 | }
51 |
52 | const entries = [];
53 | for await (const entry of walk(schemasDir, { exts: [".json"] })) {
54 | if (entry.isFile) {
55 | entries.push(entry);
56 | }
57 | }
58 |
59 | // Sort files so that the generated code is deterministic
60 | const files = entries.sort((a, b) => a.path < b.path ? -1 : 1);
61 |
62 | // Collect all schemas first
63 | const schemas = [];
64 | for (const file of files) {
65 | const jsonSchema: JSONSchema = JSON.parse(
66 | await Deno.readTextFile(file.path),
67 | );
68 | const doc = parseSchema(jsonSchema);
69 | schemas.push(doc);
70 | }
71 |
72 | if (!language || language === "typescript") {
73 | // Generate single TypeScript file with all schemas
74 | const tsContent = schemas.map((doc) =>
75 | generateTypeScript(doc, doc.title, relativePath)
76 | ).join("\n\n\n");
77 | const tsFilePath = join(tsSchemaDir, "schemas.ts");
78 | await Deno.writeTextFile(tsFilePath, tsContent);
79 | console.log(`Generated TypeScript schemas: ${tsFilePath}`);
80 | }
81 |
82 | if (!language || language === "python") {
83 | // Generate single Python file with all schemas
84 | const pyContent = schemas.map((doc) =>
85 | generatePython(doc, doc.title, relativePath)
86 | ).join("\n\n\n");
87 |
88 | // Extract all exported names and generate __all__
89 | const exportedNames = extractExportedNames(pyContent);
90 | const allContent = generateAll(exportedNames);
91 |
92 | // Insert __all__ after the header (which is in the first schema's output)
93 | const headerEndIndex = pyContent.indexOf("\n\n") + 2;
94 | const finalContent = pyContent.slice(0, headerEndIndex) + allContent +
95 | pyContent.slice(headerEndIndex);
96 |
97 | const pyFilePath = join(pySchemaDir, "schemas.py");
98 | await Deno.writeTextFile(pyFilePath, finalContent);
99 | console.log(`Generated Python schemas: ${pyFilePath}`);
100 | }
101 |
102 | // Run deno fmt on TypeScript files if they were generated
103 | if (!language || language === "typescript") {
104 | const command = new Deno.Command("deno", {
105 | args: ["fmt", tsSchemaDir],
106 | });
107 | await command.output();
108 | }
109 |
110 | // Run ruff format on Python files if they were generated
111 | if (!language || language === "python") {
112 | const command = new Deno.Command("ruff", {
113 | args: ["check", "--fix", pySchemaDir],
114 | });
115 | await command.output();
116 | }
117 | }
118 |
119 | main().catch(console.error);
120 |
--------------------------------------------------------------------------------
/scripts/generate-schema/parser.test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals, assertThrows } from "jsr:@std/assert";
2 | import { parseSchema } from "./parser.ts";
3 | import type { JSONSchema } from "../../json-schema.d.ts";
4 |
5 | // Helper to wrap a schema in a document with title and optional description
6 | function makeSchema(
7 | schema: Partial,
8 | title = "Test",
9 | description?: string,
10 | ): JSONSchema {
11 | return {
12 | $schema: "http://json-schema.org/draft-07/schema#",
13 | title,
14 | description,
15 | ...schema,
16 | } as JSONSchema;
17 | }
18 |
19 | Deno.test("parses primitive types", () => {
20 | // String type
21 | assertEquals(
22 | parseSchema(makeSchema({ type: "string" })),
23 | {
24 | type: "doc",
25 | title: "Test",
26 | root: { type: "string", optional: false, name: "Test" },
27 | definitions: {},
28 | },
29 | );
30 |
31 | // Boolean type
32 | assertEquals(
33 | parseSchema(makeSchema({ type: "boolean" })),
34 | {
35 | type: "doc",
36 | title: "Test",
37 | root: { type: "boolean", optional: false, name: "Test" },
38 | definitions: {},
39 | },
40 | );
41 |
42 | // Integer type
43 | assertEquals(
44 | parseSchema(makeSchema({ type: "integer", minimum: 0, maximum: 100 })),
45 | {
46 | type: "doc",
47 | title: "Test",
48 | root: { type: "int", minimum: 0, maximum: 100, name: "Test" },
49 | definitions: {},
50 | },
51 | );
52 |
53 | // Float type
54 | assertEquals(
55 | parseSchema(makeSchema({ type: "number", format: "double" })),
56 | {
57 | type: "doc",
58 | title: "Test",
59 | root: { type: "float", name: "Test" },
60 | definitions: {},
61 | },
62 | );
63 | });
64 |
65 | Deno.test("parses string enums", () => {
66 | // Single value enum becomes literal
67 | assertEquals(
68 | parseSchema(makeSchema({ type: "string", enum: ["test"] })),
69 | {
70 | type: "doc",
71 | title: "Test",
72 | root: { type: "literal", value: "test", name: "Test" },
73 | definitions: {},
74 | },
75 | );
76 |
77 | // Multiple values become union
78 | assertEquals(
79 | parseSchema(makeSchema({ type: "string", enum: ["a", "b"] })),
80 | {
81 | type: "doc",
82 | title: "Test",
83 | root: {
84 | type: "enum",
85 | name: "Test",
86 | members: ["a", "b"],
87 | },
88 | definitions: {},
89 | },
90 | );
91 | });
92 |
93 | Deno.test("parses simple objects", () => {
94 | assertEquals(
95 | parseSchema(makeSchema({
96 | type: "object",
97 | properties: {
98 | name: { type: "string" },
99 | age: { type: "integer" },
100 | },
101 | required: ["name"],
102 | })),
103 | {
104 | type: "doc",
105 | title: "Test",
106 | root: {
107 | name: "Test",
108 | type: "object",
109 | properties: [
110 | {
111 | key: "name",
112 | required: true,
113 | value: { type: "string", optional: false },
114 | },
115 | {
116 | key: "age",
117 | required: false,
118 | value: {
119 | type: "int",
120 | },
121 | },
122 | ],
123 | },
124 | definitions: {},
125 | },
126 | );
127 | });
128 |
129 | Deno.test("parses discriminated unions", () => {
130 | assertEquals(
131 | parseSchema(makeSchema({
132 | oneOf: [
133 | {
134 | type: "object",
135 | required: ["$type"],
136 | properties: {
137 | $type: { type: "string", enum: ["a"] },
138 | value: { type: "string" },
139 | },
140 | },
141 | {
142 | type: "object",
143 | required: ["$type"],
144 | properties: {
145 | $type: { type: "string", enum: ["b"] },
146 | count: { type: "integer" },
147 | },
148 | },
149 | ],
150 | })),
151 | {
152 | type: "doc",
153 | title: "Test",
154 | root: {
155 | name: "Test",
156 | type: "descriminated-union",
157 | descriminator: "$type",
158 | members: {
159 | a: [{
160 | value: { type: "string", optional: false },
161 | required: false,
162 | key: "value",
163 | }],
164 | b: [{
165 | key: "count",
166 | value: { type: "int" },
167 | required: false,
168 | }],
169 | },
170 | },
171 | definitions: {},
172 | },
173 | );
174 | });
175 |
176 | Deno.test("parses references", () => {
177 | assertEquals(
178 | parseSchema(makeSchema({
179 | definitions: {
180 | Point: {
181 | description: "A point in 2D space",
182 | type: "object",
183 | properties: {
184 | x: { type: "integer", description: "The x coordinate" },
185 | y: { type: "integer", description: "The y coordinate" },
186 | },
187 | required: ["x", "y"],
188 | },
189 | },
190 | $ref: "#/definitions/Point",
191 | })),
192 | {
193 | type: "doc",
194 | title: "Test",
195 | root: {
196 | type: "reference",
197 | name: "Test",
198 | },
199 | definitions: {
200 | Point: {
201 | name: "Point",
202 | description: "A point in 2D space",
203 | type: "object",
204 | properties: [
205 | {
206 | description: "The x coordinate",
207 | key: "x",
208 | required: true,
209 | value: {
210 | type: "int",
211 | },
212 | },
213 | {
214 | description: "The y coordinate",
215 | key: "y",
216 | required: true,
217 |
218 | value: {
219 | type: "int",
220 | },
221 | },
222 | ],
223 | },
224 | },
225 | },
226 | );
227 | });
228 |
229 | Deno.test("sorts definitions topologically", () => {
230 | assertEquals(
231 | parseSchema(makeSchema({
232 | definitions: {
233 | Container: {
234 | type: "object",
235 | properties: {
236 | point: { $ref: "#/definitions/Point" },
237 | size: { $ref: "#/definitions/Size" },
238 | },
239 | },
240 | Size: {
241 | type: "object",
242 | properties: {
243 | width: { type: "integer" },
244 | height: { type: "integer" },
245 | },
246 | },
247 | Point: {
248 | type: "object",
249 | properties: {
250 | x: { type: "integer" },
251 | y: { type: "integer" },
252 | },
253 | },
254 | },
255 | $ref: "#/definitions/Container",
256 | })),
257 | {
258 | type: "doc",
259 | title: "Test",
260 | root: {
261 | type: "reference",
262 | name: "Test",
263 | },
264 | definitions: {
265 | // Point and Size should come before Container since Container depends on them
266 | Point: {
267 | name: "Point",
268 | type: "object",
269 | properties: [
270 | {
271 | key: "x",
272 | required: false,
273 | value: { type: "int" },
274 | },
275 | {
276 | key: "y",
277 | required: false,
278 | value: { type: "int" },
279 | },
280 | ],
281 | },
282 | Size: {
283 | name: "Size",
284 | type: "object",
285 | properties: [
286 | {
287 | key: "width",
288 | required: false,
289 | value: { type: "int" },
290 | },
291 | {
292 | key: "height",
293 | required: false,
294 | value: { type: "int" },
295 | },
296 | ],
297 | },
298 | Container: {
299 | name: "Container",
300 | type: "object",
301 | properties: [
302 | {
303 | key: "point",
304 | required: false,
305 | value: { type: "reference", name: "Point" },
306 | },
307 | {
308 | key: "size",
309 | required: false,
310 | value: { type: "reference", name: "Size" },
311 | },
312 | ],
313 | },
314 | },
315 | },
316 | );
317 | });
318 |
319 | Deno.test("detects circular references", () => {
320 | assertThrows(
321 | () =>
322 | parseSchema(makeSchema({
323 | definitions: {
324 | A: {
325 | type: "object",
326 | properties: {
327 | b: { $ref: "#/definitions/B" },
328 | },
329 | },
330 | B: {
331 | type: "object",
332 | properties: {
333 | a: { $ref: "#/definitions/A" },
334 | },
335 | },
336 | },
337 | $ref: "#/definitions/A",
338 | })),
339 | Error,
340 | "Circular reference detected",
341 | );
342 | });
343 |
--------------------------------------------------------------------------------
/scripts/generate-schema/parser.ts:
--------------------------------------------------------------------------------
1 | import { match, P } from "npm:ts-pattern";
2 | import type { JSONSchema, JSONSchemaTypeName } from "../../json-schema.d.ts";
3 | import { assert } from "jsr:@std/assert";
4 |
5 | // defining an IR
6 | export interface Doc {
7 | type: "doc";
8 | title: string;
9 | description?: string;
10 | root: Node;
11 | definitions: Record;
12 | }
13 | export type Node =
14 | & { name?: string; description?: string }
15 | & (
16 | | {
17 | type: "descriminated-union";
18 | name?: string;
19 | descriminator: string;
20 | members: Record;
26 | }
27 | | {
28 | type: "object";
29 | name?: string;
30 | properties: {
31 | key: string;
32 | required: boolean;
33 | description?: string;
34 | value: Node;
35 | }[];
36 | }
37 | | { type: "intersection"; name?: string; members: Node[] }
38 | | { type: "union"; name?: string; members: Node[] }
39 | | { type: "enum"; members: string[] }
40 | | { type: "record"; valueType: string }
41 | | { type: "boolean"; optional?: boolean }
42 | | { type: "string"; optional?: boolean }
43 | | { type: "literal"; value: string }
44 | | { type: "int"; minimum?: number; maximum?: number }
45 | | { type: "float"; minimum?: number; maximum?: number }
46 | | { type: "reference"; name: string }
47 | | { type: "unknown" }
48 | );
49 |
50 | // Find all references in a node recursively
51 | function findReferences(node: Node): Set {
52 | const refs = new Set();
53 |
54 | if (node.type === "reference") {
55 | refs.add(node.name);
56 | } else if (node.type === "descriminated-union") {
57 | for (const member of Object.values(node.members).flat()) {
58 | for (const ref of findReferences(member.value)) {
59 | refs.add(ref);
60 | }
61 | }
62 | } else if (
63 | node.type === "union" ||
64 | node.type === "intersection"
65 | ) {
66 | for (const member of node.members) {
67 | for (const ref of findReferences(member)) {
68 | refs.add(ref);
69 | }
70 | }
71 | } else if (node.type === "object") {
72 | for (const prop of node.properties) {
73 | for (const ref of findReferences(prop.value)) {
74 | refs.add(ref);
75 | }
76 | }
77 | }
78 |
79 | return refs;
80 | }
81 |
82 | // Detect cycles in the dependency graph
83 | function detectCycle(
84 | graph: Map>,
85 | node: string,
86 | visited: Set,
87 | path: Set,
88 | ): string[] | null {
89 | if (path.has(node)) {
90 | const cycle = Array.from(path);
91 | const startIdx = cycle.indexOf(node);
92 | return cycle.slice(startIdx).concat(node);
93 | }
94 |
95 | if (visited.has(node)) return null;
96 |
97 | visited.add(node);
98 | path.add(node);
99 |
100 | const deps = graph.get(node) || new Set();
101 | for (const dep of deps) {
102 | const cycle = detectCycle(graph, dep, visited, path);
103 | if (cycle) return cycle;
104 | }
105 |
106 | path.delete(node);
107 | return null;
108 | }
109 |
110 | // Sort definitions topologically
111 | function sortDefinitions(
112 | definitions: Record,
113 | ): Record {
114 | // Build dependency graph
115 | const graph = new Map>();
116 | for (const [name, def] of Object.entries(definitions)) {
117 | graph.set(name, findReferences(def));
118 | }
119 |
120 | // Check for cycles
121 | const cycle = detectCycle(
122 | graph,
123 | Object.keys(definitions)[0],
124 | new Set(),
125 | new Set(),
126 | );
127 | if (cycle) {
128 | throw new Error(`Circular reference detected: ${cycle.join(" -> ")}`);
129 | }
130 |
131 | // Topological sort
132 | const sorted: string[] = [];
133 | const visited = new Set();
134 |
135 | function visit(name: string) {
136 | if (visited.has(name)) return;
137 | visited.add(name);
138 |
139 | const deps = graph.get(name) || new Set();
140 | for (const dep of deps) {
141 | visit(dep);
142 | }
143 |
144 | sorted.push(name);
145 | }
146 |
147 | for (const name of Object.keys(definitions)) {
148 | visit(name);
149 | }
150 |
151 | // Reconstruct definitions object in sorted order
152 | return Object.fromEntries(
153 | sorted.map((name) => [name, { ...definitions[name], name }]),
154 | );
155 | }
156 |
157 | export const isComplexType = (node: Node) => {
158 | return node.type === "descriminated-union" || node.type === "union" ||
159 | node.type === "intersection" || node.type === "object";
160 | };
161 |
162 | const isDescriminatedUnion = (def: JSONSchema[] | undefined) => {
163 | return def?.[0]?.required?.[0]?.startsWith("$");
164 | };
165 |
166 | const isOptionalType =
167 | (typeOf: string) =>
168 | (type: JSONSchemaTypeName | JSONSchemaTypeName[] | undefined) => {
169 | if (type && Array.isArray(type) && type[0] === typeOf) {
170 | return true;
171 | }
172 | return false;
173 | };
174 |
175 | const flattenUnion = (union: Node[], member: Node) => {
176 | if (member.type === "union") {
177 | return union.concat(member.members);
178 | }
179 | return union.concat(member);
180 | };
181 |
182 | export function parseSchema(schema: JSONSchema): Doc {
183 | const nodeToIR = (node: JSONSchema): Node => {
184 | return match(node)
185 | .with({ $ref: P.string }, (node) => ({
186 | type: "reference" as const,
187 | name: node.$ref.split("/").pop()!,
188 | }))
189 | .with(
190 | {
191 | type: P.union("boolean", P.when(isOptionalType("boolean"))),
192 | },
193 | (node) =>
194 | ({
195 | type: "boolean" as const,
196 | optional: !!node.default,
197 | }) as const,
198 | )
199 | .with({ type: "integer" }, (node) => ({
200 | type: "int" as const,
201 | ...(node.minimum !== undefined && { minimum: node.minimum }),
202 | ...(node.maximum !== undefined && { maximum: node.maximum }),
203 | }))
204 | .with({ type: "number", format: "double" }, (node) => ({
205 | type: "float" as const,
206 | ...(node.minimum !== undefined && { minimum: node.minimum }),
207 | ...(node.maximum !== undefined && { maximum: node.maximum }),
208 | }))
209 | .with(
210 | { type: P.union("string", P.when(isOptionalType("string"))) },
211 | (node) => {
212 | const isOptional =
213 | Array.isArray(node.type) && node.type.includes("null") || false;
214 | if (node.enum) {
215 | if (node.enum.length === 1) {
216 | return {
217 | type: "literal" as const,
218 | value: node.enum[0] as string,
219 | };
220 | }
221 | return {
222 | type: "enum" as const,
223 | members: node.enum as string[],
224 | };
225 | }
226 | return ({
227 | type: "string" as const,
228 | optional: !!node.default || isOptional,
229 | });
230 | },
231 | )
232 | .with(
233 | { oneOf: P.when(isDescriminatedUnion) },
234 | (node) => {
235 | const descriminator = (node.oneOf?.[0] as JSONSchema).required?.[0]!;
236 | return ({
237 | type: "descriminated-union" as const,
238 | descriminator,
239 | members: Object.fromEntries(
240 | node.oneOf?.map((v) => {
241 | assert(
242 | v.type === "object",
243 | "Descriminated union must have an object member",
244 | );
245 | assert(
246 | v.properties,
247 | "Descriminated union arms must have properties",
248 | );
249 | const name = v.properties[descriminator]?.enum?.[0] as string;
250 | delete v.properties[descriminator];
251 | assert(name, "Descriminated union must have a name");
252 | return [
253 | name,
254 | Object.entries(v.properties ?? {}).map((
255 | [key, value],
256 | ) => ({
257 | key,
258 | required: v.required?.includes(key) ?? false,
259 | ...(value.description &&
260 | { description: value.description }),
261 | value: nodeToIR(value as JSONSchema),
262 | })),
263 | ];
264 | }) ?? [],
265 | ),
266 | });
267 | },
268 | )
269 | .with({ allOf: P.array() }, (node) => {
270 | if (node.allOf?.length === 1) {
271 | return nodeToIR(node.allOf[0] as JSONSchema);
272 | }
273 | return {
274 | type: "intersection" as const,
275 | members: node.allOf?.map((v) => nodeToIR(v as JSONSchema)) ?? [],
276 | };
277 | })
278 | .with(
279 | P.union({ oneOf: P.array() }, { anyOf: P.array() }),
280 | (node) => {
281 | if (
282 | node.anyOf && node.anyOf.length === 2 &&
283 | node.anyOf[1].type === "null"
284 | ) {
285 | return nodeToIR(node.anyOf[0] as JSONSchema);
286 | }
287 | const union = {
288 | type: "union" as const,
289 | members:
290 | ((node.oneOf ?? node.anyOf)?.map((v) =>
291 | nodeToIR(v as JSONSchema)
292 | ) ?? [])
293 | .filter((v) => v.type !== "unknown")
294 | .reduce(flattenUnion, [] as Node[]),
295 | };
296 | if (node.properties) {
297 | return ({
298 | type: "intersection" as const,
299 | members: [
300 | {
301 | type: "object" as const,
302 | properties: Object.entries(node.properties ?? {}).map((
303 | [key, value],
304 | ) => ({
305 | key,
306 | required: node.required?.includes(key) ?? false,
307 | ...(value.description &&
308 | { description: value.description }),
309 | value: nodeToIR(value as JSONSchema),
310 | })),
311 | },
312 | union,
313 | ],
314 | });
315 | }
316 | return union;
317 | },
318 | )
319 | .with(
320 | { type: P.union("object", P.when(isOptionalType("object"))) },
321 | () => {
322 | if (Object.keys(node.properties ?? {}).length === 0) {
323 | if (
324 | typeof node.additionalProperties === "object" &&
325 | "type" in node.additionalProperties
326 | ) {
327 | return {
328 | type: "record" as const,
329 | ...(node.description && { description: node.description }),
330 | valueType: typeof node.additionalProperties.type === "string"
331 | ? node.additionalProperties.type
332 | : "unknown",
333 | };
334 | }
335 | return {
336 | type: "record" as const,
337 | ...(node.description && { description: node.description }),
338 | valueType: "unknown",
339 | };
340 | }
341 | return ({
342 | type: "object" as const,
343 | ...(node.description && { description: node.description }),
344 | properties: Object.entries(node.properties ?? {}).map((
345 | [key, value],
346 | ) => ({
347 | key,
348 | required: Boolean(
349 | node.required?.includes(key) ||
350 | "anyOf" in value &&
351 | !value.anyOf?.some((v) => v.type === "null"),
352 | ) || false,
353 | ...(value.description && { description: value.description }),
354 | value: nodeToIR(value as JSONSchema),
355 | })),
356 | });
357 | },
358 | )
359 | .otherwise(() => ({ type: "unknown" }));
360 | };
361 |
362 | const definitions = Object.fromEntries(
363 | Object.entries(schema.definitions ?? {}).map((
364 | [name, type],
365 | ) => [name, {
366 | ...(type.description && {
367 | description: type.description,
368 | name,
369 | }),
370 | ...nodeToIR(type as JSONSchema),
371 | }]),
372 | ) ?? {};
373 |
374 | return {
375 | type: "doc",
376 | title: schema.title!,
377 | ...(schema.description && { description: schema.description }),
378 | root: { ...nodeToIR(schema), name: schema.title! },
379 | definitions: sortDefinitions(definitions),
380 | };
381 | }
382 |
--------------------------------------------------------------------------------
/scripts/generate-schema/printer.ts:
--------------------------------------------------------------------------------
1 | import { match } from "npm:ts-pattern";
2 | import { Doc, Node } from "./parser.ts";
3 | import {
4 | blue,
5 | bold,
6 | dimmed,
7 | type Formatter,
8 | green,
9 | mix,
10 | yellow,
11 | } from "jsr:@coven/terminal";
12 |
13 | const comment = mix(yellow, dimmed);
14 | const string = green;
15 | const type = blue;
16 | const kind = bold;
17 |
18 | function wrapText(
19 | text: string,
20 | maxLength: number,
21 | indent: string,
22 | formatter?: Formatter,
23 | ): string[] {
24 | const words = text.split(" ");
25 | const lines: string[] = [];
26 | let currentLine = "";
27 |
28 | for (const word of words) {
29 | if (currentLine.length + word.length + 1 <= maxLength) {
30 | currentLine += (currentLine.length === 0 ? "" : " ") + word;
31 | } else {
32 | lines.push(currentLine);
33 | currentLine = word;
34 | }
35 | }
36 | if (currentLine.length > 0) {
37 | lines.push(currentLine);
38 | }
39 | return lines
40 | .map((line) => formatter ? formatter`${line}` : line)
41 | .map((line, i) => i === 0 ? line : indent + line);
42 | }
43 |
44 | function printNodeIR(
45 | node: Node,
46 | prefix: string = "",
47 | isLast: boolean = true,
48 | ): string {
49 | const marker = isLast ? "└── " : "├── ";
50 | const childPrefix = prefix + (isLast ? " " : "│ ");
51 |
52 | return prefix + marker + match(node)
53 | .with({ type: "reference" }, ({ name }) => kind`${name}\n`)
54 | .with({ type: "descriminated-union" }, ({ descriminator, members }) => {
55 | let output = kind`discriminated-union` + ` (by ${descriminator})\n`;
56 | Object.entries(members).forEach(([name, properties], index) => {
57 | output += printNodeIR(
58 | { type: "object", name, properties },
59 | childPrefix,
60 | index === Object.values(members).length - 1,
61 | );
62 | });
63 | return output;
64 | })
65 | .with({ type: "intersection" }, ({ members }) => {
66 | let output = kind`intersection\n`;
67 | members.forEach((member, index) => {
68 | output += printNodeIR(
69 | member,
70 | childPrefix,
71 | index === members.length - 1,
72 | );
73 | });
74 | return output;
75 | })
76 | .with({ type: "union" }, ({ members }) => {
77 | let output = kind`union\n`;
78 | members.forEach((member, index) => {
79 | output += printNodeIR(
80 | member,
81 | childPrefix,
82 | index === members.length - 1,
83 | );
84 | });
85 | return output;
86 | })
87 | .with({ type: "object" }, ({ name, properties }) => {
88 | let output = kind`${name ?? "object"}\n`;
89 | properties.forEach(({ key, required, value, description }, index) => {
90 | description = description?.split("\n")[0];
91 | const propDesc = `${key}${required ? "" : "?"}: `;
92 |
93 | if (description) {
94 | const wrappedDesc = wrapText(
95 | description,
96 | 96,
97 | childPrefix + "│ ",
98 | comment,
99 | )
100 | .join("\n");
101 | output += childPrefix + "│ " + `${wrappedDesc}\n`;
102 | output += childPrefix +
103 | (index === properties.length - 1 ? "└── " : "├── ");
104 | } else {
105 | output += childPrefix +
106 | (index === properties.length - 1 ? "└── " : "├── ");
107 | }
108 |
109 | const valueStr = match(value)
110 | .with(
111 | { type: "boolean" },
112 | ({ optional }) => type`boolean${optional ? "?" : ""}`,
113 | )
114 | .with(
115 | { type: "string" },
116 | ({ optional }) => type`string${optional ? "?" : ""}`,
117 | )
118 | .with({ type: "literal" }, ({ value }) => string`"${value}"`)
119 | .with(
120 | { type: "int" },
121 | ({ minimum, maximum }) =>
122 | type`int${minimum !== undefined ? ` min(${minimum})` : ""}${
123 | maximum !== undefined ? ` max(${maximum})` : ""
124 | }`,
125 | )
126 | .with(
127 | { type: "float" },
128 | ({ minimum, maximum }) =>
129 | type`float${minimum !== undefined ? ` min(${minimum})` : ""}${
130 | maximum !== undefined ? ` max(${maximum})` : ""
131 | }`,
132 | )
133 | .with(
134 | { type: "record" },
135 | ({ valueType }) => type`record`,
136 | )
137 | .with({ type: "unknown" }, () => "unknown")
138 | .with({ type: "reference" }, ({ name }) => name)
139 | .otherwise(() => {
140 | output += propDesc + "\n";
141 | return printNodeIR(
142 | value,
143 | childPrefix + (index === properties.length - 1 ? " " : "│ "),
144 | true,
145 | );
146 | });
147 |
148 | if (
149 | value.type === "union" || value.type === "intersection" ||
150 | value.type === "descriminated-union" || value.type === "object"
151 | ) {
152 | output += valueStr;
153 | } else {
154 | output += propDesc + valueStr + "\n";
155 | }
156 | });
157 | return output;
158 | })
159 | .with({ type: "enum" }, ({ members }) => {
160 | return kind`enum` + `[${members.join(",")}]\n`;
161 | })
162 | .with(
163 | { type: "record" },
164 | ({ valueType }) => `record\n`,
165 | )
166 | .with(
167 | { type: "boolean" },
168 | ({ optional }) => `boolean${optional ? "?" : ""}\n`,
169 | )
170 | .with(
171 | { type: "string" },
172 | ({ optional }) => `string${optional ? "?" : ""}\n`,
173 | )
174 | .with({ type: "literal" }, ({ value }) => `"${value}"\n`)
175 | .with(
176 | { type: "int" },
177 | ({ minimum, maximum }) =>
178 | `int${minimum !== undefined ? ` min(${minimum})` : ""}${
179 | maximum !== undefined ? ` max(${maximum})` : ""
180 | }\n`,
181 | )
182 | .with(
183 | { type: "float" },
184 | ({ minimum, maximum }) =>
185 | `float${minimum !== undefined ? ` min(${minimum})` : ""}${
186 | maximum !== undefined ? ` max(${maximum})` : ""
187 | }\n`,
188 | )
189 | .with({ type: "unknown" }, () => "unknown\n")
190 | .exhaustive();
191 | }
192 |
193 | export function printDocIR(doc: Doc): string {
194 | let result = `${doc.title}\n`;
195 | const description = doc.description?.split("\n")[0];
196 | if (description) {
197 | const wrappedDesc = wrapText(description, 96, "│ description: ", comment)
198 | .join("\n");
199 | result += "│ description: " + `${wrappedDesc}\n`;
200 | }
201 | result += printNodeIR(doc.root, "", true);
202 | for (const [name, definition] of Object.entries(doc.definitions)) {
203 | result += `${name}\n`;
204 | const description = definition.description?.split("\n")[0];
205 | if (description) {
206 | const wrappedDesc = wrapText(
207 | description,
208 | 96,
209 | "│ description: ",
210 | comment,
211 | ).join(
212 | "\n",
213 | );
214 | result += "│ description: " + `${wrappedDesc}\n`;
215 | }
216 | result += printNodeIR(definition, "", true);
217 | }
218 | return result;
219 | }
220 |
--------------------------------------------------------------------------------
/scripts/sync-versions.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Synchronizes version numbers across the project:
3 | * - Updates BIN_VERSION in Deno client (main.ts)
4 | * - Updates package version in Python client (pyproject.toml)
5 | * - Updates BIN_VERSION in Python client (__init__.py)
6 | *
7 | * All versions are synchronized with the main version from Cargo.toml
8 | */
9 |
10 | import { parse } from "jsr:@std/toml";
11 |
12 | // Read the source version from Cargo.toml
13 | const latestVersion = await Deno
14 | .readTextFile("./Cargo.toml").then((text) =>
15 | parse(text) as { package: { version: string } }
16 | ).then((config) => config.package.version);
17 |
18 | // ===== Update Deno Client Version =====
19 | const denoPath = "./src/clients/deno/main.ts";
20 | const denoContent = await Deno.readTextFile(denoPath);
21 |
22 | const updatedDenoContent = denoContent.replace(
23 | /const BIN_VERSION = "[^"]+"/,
24 | `const BIN_VERSION = "${latestVersion}"`,
25 | );
26 |
27 | await Deno.writeTextFile(denoPath, updatedDenoContent);
28 | console.log(`✓ Updated Deno BIN_VERSION to ${latestVersion}`);
29 |
30 | // ===== Update Python Client BIN_VERSION =====
31 | const pythonInitPath = "./src/clients/python/src/justbe_webview/__init__.py";
32 | const pythonInitContent = await Deno.readTextFile(pythonInitPath);
33 |
34 | const updatedPythonInitContent = pythonInitContent.replace(
35 | /BIN_VERSION = "[^"]+"/,
36 | `BIN_VERSION = "${latestVersion}"`,
37 | );
38 |
39 | await Deno.writeTextFile(pythonInitPath, updatedPythonInitContent);
40 | console.log(`✓ Updated Python BIN_VERSION to ${latestVersion}`);
41 |
42 | console.log(`\n🎉 Successfully synchronized all versions to ${latestVersion}`);
43 |
--------------------------------------------------------------------------------
/sg/rules/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zephraph/webview/f237514c9565568849c3accbf46f8cc506103785/sg/rules/.gitkeep
--------------------------------------------------------------------------------
/sg/rules/no-mixed-enums.yml:
--------------------------------------------------------------------------------
1 | id: no-mixed-enum
2 | language: rust
3 | severity: error
4 | message: Don't mix call style and object style enum variants
5 | rule:
6 | matches: enum
7 | all:
8 | - has:
9 | stopBy: end
10 | all:
11 | - kind: field_declaration_list
12 | - has:
13 | stopBy: end
14 | all:
15 | - kind: ordered_field_declaration_list
16 | utils:
17 | enum:
18 | any:
19 | - pattern: enum $VARIANT{$$$BODY}
20 | - pattern: pub enum $VARIANT{$$$BODY}
21 |
--------------------------------------------------------------------------------
/sgconfig.yml:
--------------------------------------------------------------------------------
1 | ruleDirs:
2 | - ./sg/rules
3 |
--------------------------------------------------------------------------------
/src/bin/generate_schemas.rs:
--------------------------------------------------------------------------------
1 | use schemars::schema_for;
2 | use std::fs::File;
3 | use std::io::Write;
4 | use webview::{Message, Options, Request, Response};
5 |
6 | fn main() {
7 | let schemas = [
8 | ("WebViewOptions", schema_for!(Options)),
9 | ("WebViewMessage", schema_for!(Message)),
10 | ("WebViewRequest", schema_for!(Request)),
11 | ("WebViewResponse", schema_for!(Response)),
12 | ];
13 |
14 | for (name, schema) in schemas {
15 | let schema_json = serde_json::to_string_pretty(&schema).unwrap();
16 | let mut file = File::create(format!("schemas/{}.json", name)).unwrap();
17 | file.write_all(schema_json.as_bytes()).unwrap();
18 | println!("Generated schema for {}", name);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/bin/webview.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 | use tracing::error;
3 | use webview::{run, Options};
4 |
5 | fn main() {
6 | let subscriber = tracing_subscriber::fmt()
7 | .with_env_filter(env::var("LOG_LEVEL").unwrap_or_else(|_| "info".to_string()))
8 | .with_writer(std::io::stderr)
9 | .finish();
10 | tracing::subscriber::set_global_default(subscriber).unwrap();
11 |
12 | let args: Vec = env::args().collect();
13 |
14 | let webview_options: Options = match serde_json::from_str(&args[1]) {
15 | Ok(options) => options,
16 | Err(e) => {
17 | error!("Failed to parse webview options: {:?}", e);
18 | std::process::exit(1);
19 | }
20 | };
21 |
22 | if let Err(e) = run(webview_options) {
23 | error!("Webview error: {:?}", e);
24 | std::process::exit(1);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/clients/deno/README.md:
--------------------------------------------------------------------------------
1 | # @justbe/webview Deno Client
2 |
3 | A light, cross-platform library for building web-based desktop apps with
4 | [Deno](https://deno.com/).
5 |
6 | ## Installation
7 |
8 | ```typescript
9 | import { createWebView } from "jsr:@justbe/webview";
10 | ```
11 |
12 | ## Example
13 |
14 | ```typescript
15 | import { createWebView } from "jsr:@justbe/webview";
16 |
17 | using webview = await createWebView({
18 | title: "Example",
19 | html: "Hello, World! ",
20 | devtools: true,
21 | });
22 |
23 | webview.on("started", async () => {
24 | await webview.openDevTools();
25 | await webview.eval("console.log('This is printed from eval!')");
26 | });
27 |
28 | await webview.waitUntilClosed();
29 | ```
30 |
31 | You can run this yourself with:
32 |
33 | ```sh
34 | deno run https://raw.githubusercontent.com/zephraph/webview/refs/heads/main/examples/simple.ts
35 | ```
36 |
37 | Check out the [examples directory](examples/) for more examples.
38 |
39 | ## Permissions
40 |
41 | When executing this package, it checks to see if you have the required binary
42 | for interfacing with the OS's webview. If it doesn't exist, it downloads it to a
43 | cache directory and executes it. This yields a few different permission code
44 | paths to be aware of.
45 |
46 | ### Binary not in cache
47 |
48 | This will be true of a first run of the package. These are the following
49 | permission requests you can expect to see:
50 |
51 | - Read HOME env -- Used to locate the cache directory
52 | - Read /webview/webview- -- Tries to read the binary from cache
53 | - net to github.com:443 -- Connects to GitHub releases to try to download the
54 | binary (will be redirected)
55 | - net to objects.githubusercontent.com:443 -- GitHub's CDN for the actual
56 | download
57 | - Read /webview/ -- Reads the cache directory
58 | - Write /webview/webview- -- Writes the binary
59 | - Run /webview/webview- -- Runs the binary
60 |
61 | ### Binary cached
62 |
63 | On subsequent runs you can expect fewer permission requests:
64 |
65 | - Read HOME env -- Use to locate the cache directory
66 | - Read /deno-webview/deno-webview-
67 | - Run /deno-webview/deno-webview-
68 |
69 | ### Using a Custom Binary
70 |
71 | You can specify a custom binary path using the `WEBVIEW_BIN` environment
72 | variable. When set and allowed, this will bypass the default binary resolution
73 | process. In this case, only one permission is needed:
74 |
75 | - Run
76 |
77 | Note that this environment variable will never be _explicitly_ requested. If the
78 | script detects it's not allowed to read this env var it just skips this code
79 | path altogether.
80 |
--------------------------------------------------------------------------------
/src/clients/deno/deno.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@justbe/webview",
3 | "exports": "./main.ts",
4 | "license": "MIT",
5 | "version": "1.0.1-rc.2",
6 | "publish": {
7 | "include": ["README.md", "LICENSE", "*.ts", "schemas/*.ts"]
8 | },
9 | "imports": {
10 | "ts-pattern": "jsr:@gabriel/ts-pattern@^5.6.2",
11 | "tracing": "jsr:@bcheidemann/tracing@^0.6.3",
12 | "jsr:@std/fs": "jsr:@std/fs@^1.0.3",
13 | "jsr:@std/path": "jsr:@std/path@^1.0.6",
14 | "npm:zod": "npm:zod@^3.23.8",
15 | "npm:type-fest": "npm:type-fest@^4.26.1"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/clients/deno/deno.lock:
--------------------------------------------------------------------------------
1 | {
2 | "version": "4",
3 | "specifiers": {
4 | "jsr:@bcheidemann/parse-params@0.5": "0.5.0",
5 | "jsr:@bcheidemann/tracing@~0.6.3": "0.6.3",
6 | "jsr:@gabriel/ts-pattern@^5.6.2": "5.6.2",
7 | "jsr:@std/assert@0.226": "0.226.0",
8 | "jsr:@std/assert@0.226.0": "0.226.0",
9 | "jsr:@std/fmt@~0.225.4": "0.225.6",
10 | "jsr:@std/fs@^1.0.3": "1.0.6",
11 | "jsr:@std/internal@1": "1.0.5",
12 | "jsr:@std/path@^1.0.6": "1.0.8",
13 | "jsr:@std/path@^1.0.8": "1.0.8",
14 | "npm:acorn@8.12.0": "8.12.0",
15 | "npm:type-fest@^4.26.1": "4.30.2",
16 | "npm:zod@^3.23.8": "3.23.8"
17 | },
18 | "jsr": {
19 | "@bcheidemann/parse-params@0.5.0": {
20 | "integrity": "a13a95163fad0cbd34890cb5d43df3466cc96e97e76674052592a90bbf56bbab",
21 | "dependencies": [
22 | "jsr:@std/assert@0.226.0",
23 | "npm:acorn"
24 | ]
25 | },
26 | "@bcheidemann/tracing@0.6.3": {
27 | "integrity": "d6f38e77ef142a88b9eaeff3a2df54a8a24b2226fff747119ddc40726a05abeb",
28 | "dependencies": [
29 | "jsr:@bcheidemann/parse-params",
30 | "jsr:@std/assert@0.226",
31 | "jsr:@std/fmt"
32 | ]
33 | },
34 | "@gabriel/ts-pattern@5.6.2": {
35 | "integrity": "49a1069523e2ba53d024d6b17a30029f8ffc80b24fec86d724d986a9774a7197"
36 | },
37 | "@std/assert@0.226.0": {
38 | "integrity": "0dfb5f7c7723c18cec118e080fec76ce15b4c31154b15ad2bd74822603ef75b3",
39 | "dependencies": [
40 | "jsr:@std/internal"
41 | ]
42 | },
43 | "@std/fmt@0.225.6": {
44 | "integrity": "aba6aea27f66813cecfd9484e074a9e9845782ab0685c030e453a8a70b37afc8"
45 | },
46 | "@std/fs@1.0.6": {
47 | "integrity": "42b56e1e41b75583a21d5a37f6a6a27de9f510bcd36c0c85791d685ca0b85fa2",
48 | "dependencies": [
49 | "jsr:@std/path@^1.0.8"
50 | ]
51 | },
52 | "@std/internal@1.0.5": {
53 | "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba"
54 | },
55 | "@std/path@1.0.8": {
56 | "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be"
57 | }
58 | },
59 | "npm": {
60 | "acorn@8.12.0": {
61 | "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw=="
62 | },
63 | "type-fest@4.30.2": {
64 | "integrity": "sha512-UJShLPYi1aWqCdq9HycOL/gwsuqda1OISdBO3t8RlXQC4QvtuIz4b5FCfe2dQIWEpmlRExKmcTBfP1r9bhY7ig=="
65 | },
66 | "zod@3.23.8": {
67 | "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="
68 | }
69 | },
70 | "remote": {
71 | "https://deno.land/x/esbuild@v0.24.0/wasm.js": "5cd1dd0c40214d06bd86177b4ffebfbb219a22114f78c14c23606f7ad216c174"
72 | },
73 | "workspace": {
74 | "dependencies": [
75 | "jsr:@bcheidemann/tracing@~0.6.3",
76 | "jsr:@gabriel/ts-pattern@^5.6.2",
77 | "jsr:@std/fs@^1.0.3",
78 | "jsr:@std/path@^1.0.6",
79 | "npm:type-fest@^4.26.1",
80 | "npm:zod@^3.23.8"
81 | ]
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/clients/deno/examples/ipc.ts:
--------------------------------------------------------------------------------
1 | import { createWebView } from "../main.ts";
2 |
3 | using webview = await createWebView({
4 | title: "Simple",
5 | load: {
6 | html:
7 | 'Click me ',
8 | },
9 | ipc: true,
10 | });
11 |
12 | webview.on("ipc", ({ message }) => {
13 | console.log(message);
14 | });
15 |
16 | await webview.waitUntilClosed();
17 |
--------------------------------------------------------------------------------
/src/clients/deno/examples/load-html.ts:
--------------------------------------------------------------------------------
1 | import { createWebView } from "../main.ts";
2 |
3 | using webview = await createWebView({
4 | title: "Load Html Example",
5 | load: {
6 | html: "Initial html ",
7 | // Note: This origin is used with a custom protocol so it doesn't match
8 | // https://example.com. This doesn't need to be set, but can be useful if
9 | // you want to control resources that are scoped to a specific origin like
10 | // local storage or indexeddb.
11 | origin: "example.com",
12 | },
13 | devtools: true,
14 | });
15 |
16 | webview.on("started", async () => {
17 | await webview.openDevTools();
18 | await webview.loadHtml("Updated html! ");
19 | });
20 |
21 | await webview.waitUntilClosed();
22 |
--------------------------------------------------------------------------------
/src/clients/deno/examples/load-url.ts:
--------------------------------------------------------------------------------
1 | import { createWebView } from "../main.ts";
2 |
3 | using webview = await createWebView({
4 | title: "Load Url Example",
5 | load: {
6 | url: "https://example.com",
7 | headers: {
8 | "Content-Type": "text/html",
9 | },
10 | },
11 | userAgent: "curl/7.81.0",
12 | devtools: true,
13 | });
14 |
15 | webview.on("started", async () => {
16 | await webview.openDevTools();
17 | await sleep(2000);
18 | await webview.loadUrl("https://val.town/", {
19 | "Content-Type": "text/html",
20 | });
21 | });
22 |
23 | await webview.waitUntilClosed();
24 |
25 | function sleep(ms: number) {
26 | return new Promise((resolve) => setTimeout(resolve, ms));
27 | }
28 |
--------------------------------------------------------------------------------
/src/clients/deno/examples/simple.ts:
--------------------------------------------------------------------------------
1 | import { createWebView } from "../main.ts";
2 |
3 | using webview = await createWebView({
4 | title: "Simple",
5 | devtools: true,
6 | load: {
7 | html: "Hello, World! ",
8 | },
9 | initializationScript:
10 | "console.log('This is printed from initializationScript!')",
11 | });
12 |
13 | webview.on("started", async () => {
14 | await webview.setTitle("Title set from Deno");
15 | await webview.getTitle();
16 | await webview.openDevTools();
17 | await webview.eval("console.log('This is printed from eval!')");
18 | });
19 |
20 | await webview.waitUntilClosed();
21 |
--------------------------------------------------------------------------------
/src/clients/deno/examples/tldraw.ts:
--------------------------------------------------------------------------------
1 | import { createWebView } from "../main.ts";
2 | import * as esbuild from "https://deno.land/x/esbuild@v0.24.0/wasm.js";
3 |
4 | const tldrawApp = `
5 | import { Tldraw } from "tldraw";
6 | import { createRoot } from "react-dom/client";
7 |
8 | function App() {
9 | return (
10 | <>
11 |
12 |
13 |
14 | >
15 | );
16 | }
17 |
18 | createRoot(document.querySelector("main")).render( );
19 | `;
20 |
21 | const app = await esbuild.transform(tldrawApp, {
22 | loader: "jsx",
23 | jsx: "automatic",
24 | target: "esnext",
25 | format: "esm",
26 | minify: false,
27 | sourcemap: false,
28 | });
29 |
30 | using webview = await createWebView({
31 | title: "TLDraw",
32 | load: {
33 | html: `
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
53 |
56 |
57 |
58 | `,
59 | },
60 | });
61 |
62 | await webview.waitUntilClosed();
63 |
--------------------------------------------------------------------------------
/src/clients/deno/examples/window-size.ts:
--------------------------------------------------------------------------------
1 | import { createWebView } from "../main.ts";
2 |
3 | using webview = await createWebView({
4 | title: "Window Size",
5 | load: {
6 | html: `
7 | Window Sizes
8 |
9 | Maximize
10 | Minimize
11 | Fullscreen
12 |
13 | `,
14 | },
15 | size: {
16 | height: 200,
17 | width: 800,
18 | },
19 | ipc: true,
20 | });
21 |
22 | webview.on("ipc", ({ message }) => {
23 | switch (message) {
24 | case "maximize":
25 | webview.maximize();
26 | break;
27 | case "minimize":
28 | webview.minimize();
29 | break;
30 | case "fullscreen":
31 | webview.fullscreen();
32 | break;
33 | default:
34 | console.error("Unknown message", message);
35 | }
36 | });
37 |
38 | await webview.waitUntilClosed();
39 |
--------------------------------------------------------------------------------
/src/clients/deno/main.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * A library for creating and interacting with native webview windows.
3 | *
4 | * @module
5 | *
6 | * @example
7 | * ```ts
8 | * import { createWebView } from "jsr:@justbe/webview";
9 | *
10 | * using webview = await createWebView({
11 | * title: "Example",
12 | * html: "Hello, World! ",
13 | * devtools: true
14 | * });
15 | *
16 | * webview.on("started", async () => {
17 | * await webview.openDevTools();
18 | * await webview.eval("console.log('This is printed from eval!')");
19 | * });
20 | *
21 | * await webview.waitUntilClosed();
22 | * ```
23 | */
24 |
25 | import { EventEmitter } from "node:events";
26 | import {
27 | Message,
28 | type Options,
29 | type Request as WebViewRequest,
30 | Response as WebViewResponse,
31 | } from "./schemas.ts";
32 | import type { Except, Simplify } from "npm:type-fest";
33 | import { join } from "jsr:@std/path";
34 | import { ensureDir, exists } from "jsr:@std/fs";
35 | import { error, FmtSubscriber, instrument, Level, trace, warn } from "tracing";
36 | import { match, P } from "ts-pattern";
37 |
38 | export * from "./schemas.ts";
39 |
40 | if (
41 | Deno.permissions.querySync({ name: "env", variable: "LOG_LEVEL" }).state ===
42 | "granted"
43 | ) {
44 | const level = match(Deno.env.get("LOG_LEVEL"))
45 | .with("trace", () => Level.TRACE)
46 | .with("debug", () => Level.DEBUG)
47 | .with("info", () => Level.INFO)
48 | .with("warn", () => Level.WARN)
49 | .with("error", () => Level.ERROR)
50 | .with("fatal", () => Level.CRITICAL)
51 | .otherwise(() => Level.INFO);
52 |
53 | FmtSubscriber.setGlobalDefault({ level, color: true });
54 | }
55 |
56 | // Should match the cargo package version
57 | /** The version of the webview binary that's expected */
58 | export const BIN_VERSION = "0.3.1";
59 |
60 | type WebViewNotification = Extract<
61 | Message,
62 | { $type: "notification" }
63 | >["data"];
64 |
65 | type ResultType = Extract;
66 |
67 | /**
68 | * A helper function for extracting the result from a webview response.
69 | * Throws if the response includes unexpected results.
70 | *
71 | * @param result - The result of the webview request.
72 | * @param expectedType - The format of the expected result.
73 | */
74 | function returnResult<
75 | Response extends WebViewResponse,
76 | E extends ResultType["result"]["$type"],
77 | >(
78 | result: Response,
79 | expectedType: E,
80 | ): Extract["value"] {
81 | if (result.$type === "result") {
82 | if (result.result.$type === expectedType) {
83 | // @ts-expect-error TS doesn't correctly narrow this type, but it's correct
84 | return result.result.value;
85 | }
86 | throw new Error(`unexpected result type: ${result.result.$type}`);
87 | }
88 | throw new Error(`unexpected response: ${result.$type}`);
89 | }
90 |
91 | /**
92 | * A helper function for acknowledging a webview response.
93 | * Throws if the response includes unexpected results.
94 | */
95 | const returnAck = (result: WebViewResponse) => {
96 | return match(result)
97 | .with({ $type: "ack" }, () => undefined)
98 | .with({ $type: "err" }, (err) => {
99 | throw new Error(err.message);
100 | })
101 | .otherwise(() => {
102 | throw new Error(`unexpected response: ${result.$type}`);
103 | });
104 | };
105 |
106 | async function getWebViewBin(options: Options) {
107 | if (
108 | Deno.permissions.querySync({ name: "env", variable: "WEBVIEW_BIN" })
109 | .state === "granted"
110 | ) {
111 | const binPath = Deno.env.get("WEBVIEW_BIN");
112 | if (binPath) return binPath;
113 | }
114 |
115 | const flags = options.devtools
116 | ? "-devtools"
117 | : options.transparent && Deno.build.os === "darwin"
118 | ? "-transparent"
119 | : "";
120 |
121 | const cacheDir = getCacheDir();
122 | const fileName = `webview-${BIN_VERSION}${flags}${
123 | Deno.build.os === "windows" ? ".exe" : ""
124 | }`;
125 | const filePath = join(cacheDir, fileName);
126 |
127 | // Check if the file already exists in cache
128 | if (await exists(filePath)) {
129 | return filePath;
130 | }
131 |
132 | // If not in cache, download it
133 | let url =
134 | `https://github.com/zephraph/webview/releases/download/webview-v${BIN_VERSION}/webview`;
135 | url += match(Deno.build.os)
136 | .with(
137 | "darwin",
138 | () => "-mac" + (Deno.build.arch === "aarch64" ? "-arm64" : "") + flags,
139 | )
140 | .with("linux", () => "-linux" + flags)
141 | .with("windows", () => "-windows" + flags + ".exe")
142 | .otherwise(() => {
143 | throw new Error("unsupported OS");
144 | });
145 |
146 | const res = await fetch(url);
147 |
148 | // Ensure the cache directory exists
149 | await ensureDir(cacheDir);
150 |
151 | // Write the binary to disk
152 | await Deno.writeFile(filePath, new Uint8Array(await res.arrayBuffer()), {
153 | mode: 0o755,
154 | });
155 |
156 | return filePath;
157 | }
158 |
159 | // Helper function to get the OS-specific cache directory
160 | function getCacheDir(): string {
161 | return match(Deno.build.os)
162 | .with(
163 | "darwin",
164 | () => join(Deno.env.get("HOME")!, "Library", "Caches", "webview"),
165 | )
166 | .with("linux", () => join(Deno.env.get("HOME")!, ".cache", "webview"))
167 | .with(
168 | "windows",
169 | () => join(Deno.env.get("LOCALAPPDATA")!, "webview", "Cache"),
170 | )
171 | .otherwise(() => {
172 | throw new Error("Unsupported OS");
173 | });
174 | }
175 |
176 | /**
177 | * Creates a new webview window.
178 | *
179 | * Will automatically fetch the webview binary if it's not already downloaded
180 | */
181 | export async function createWebView(options: Options): Promise {
182 | const binPath = await getWebViewBin(options);
183 | return new WebView(options, binPath);
184 | }
185 |
186 | /**
187 | * A webview window. It's recommended to use the `createWebView` function
188 | * because it provides a means of automatically fetching the webview binary
189 | * that's compatible with your OS and architecture.
190 | *
191 | * Each instance of `WebView` spawns a new process that governs a single webview window.
192 | */
193 | export class WebView implements Disposable {
194 | #process: Deno.ChildProcess;
195 | #stdin: WritableStreamDefaultWriter;
196 | #stdout: ReadableStreamDefaultReader;
197 | #buffer = "";
198 | #internalEvent = new EventEmitter();
199 | #externalEvent = new EventEmitter();
200 | #messageLoop: Promise;
201 | #options: Options;
202 | #messageId = 0;
203 |
204 | /**
205 | * Creates a new webview window.
206 | *
207 | * @param options - The options for the webview.
208 | * @param webviewBinaryPath - The path to the webview binary.
209 | */
210 | constructor(options: Options, webviewBinaryPath: string) {
211 | this.#options = options;
212 | this.#process = new Deno.Command(webviewBinaryPath, {
213 | args: [JSON.stringify(options)],
214 | stdin: "piped",
215 | stdout: "piped",
216 | stderr: "inherit",
217 | }).spawn();
218 | this.#stdin = this.#process.stdin.getWriter();
219 | this.#stdout = this.#process.stdout.getReader();
220 | this.#messageLoop = this.#processMessageLoop();
221 | }
222 |
223 | #send(request: Except): Promise {
224 | const id = this.#messageId++;
225 | return new Promise((resolve) => {
226 | // Setup listener before sending the message to avoid race conditions
227 | this.#internalEvent.once(id.toString(), (event) => {
228 | const result = WebViewResponse.safeParse(event);
229 | if (result.success) {
230 | resolve(result.data);
231 | } else {
232 | resolve({ $type: "err", id, message: result.error.message });
233 | }
234 | });
235 | this.#stdin.write(
236 | new TextEncoder().encode(
237 | JSON.stringify({ ...request, id }),
238 | ),
239 | );
240 | });
241 | }
242 |
243 | @instrument()
244 | async #recv() {
245 | while (true) {
246 | const { value, done } = await this.#stdout.read();
247 | if (done) {
248 | break;
249 | }
250 | this.#buffer += new TextDecoder().decode(value);
251 |
252 | const newlineIndex = this.#buffer.indexOf("\n");
253 | if (newlineIndex === -1) {
254 | continue;
255 | }
256 | trace("buffer", { buffer: this.#buffer });
257 | const result = Message.safeParse(
258 | JSON.parse(this.#buffer.slice(0, newlineIndex)),
259 | );
260 | this.#buffer = this.#buffer.slice(newlineIndex + 1);
261 | if (result.success) {
262 | return result.data;
263 | } else {
264 | error("Error parsing message", { error: result.error });
265 | return result;
266 | }
267 | }
268 | }
269 |
270 | async #processMessageLoop() {
271 | while (true) {
272 | const result = await this.#recv();
273 | if (!result) return;
274 | match(result)
275 | .with({ error: { issues: [{ code: "invalid_type" }] } }, (result) => {
276 | error("Invalid type", { error: result.error });
277 | })
278 | .with({ error: P.nonNullable }, (result) => {
279 | error("Unknown error", { error: result.error });
280 | })
281 | .with({ $type: "notification" }, ({ data }) => {
282 | const { $type, ...body } = data;
283 | this.#externalEvent.emit($type, body);
284 | if (data.$type === "started" && data.version !== BIN_VERSION) {
285 | warn(
286 | `Expected webview to be version ${BIN_VERSION} but got ${data.version}. Some features may not work as expected.`,
287 | );
288 | }
289 | })
290 | .with({ $type: "response" }, ({ data }) => {
291 | this.#internalEvent.emit(data.id.toString(), data);
292 | })
293 | .exhaustive();
294 | }
295 | }
296 |
297 | /**
298 | * Returns a promise that resolves when the webview window is closed.
299 | */
300 | async waitUntilClosed() {
301 | await this.#messageLoop;
302 | }
303 |
304 | /**
305 | * Listens for events emitted by the webview.
306 | */
307 | on(
308 | event: E,
309 | callback: (
310 | event: Simplify<
311 | Omit, "$type">
312 | >,
313 | ) => void,
314 | ) {
315 | if (event === "ipc" && !this.#options.ipc) {
316 | throw new Error("IPC is not enabled for this webview");
317 | }
318 | this.#externalEvent.on(event, callback);
319 | }
320 |
321 | /**
322 | * Listens for a single event emitted by the webview.
323 | */
324 | once(
325 | event: E,
326 | callback: (
327 | event: Simplify<
328 | Omit, "$type">
329 | >,
330 | ) => void,
331 | ) {
332 | if (event === "ipc" && !this.#options.ipc) {
333 | throw new Error("IPC is not enabled for this webview");
334 | }
335 | this.#externalEvent.once(event, callback);
336 | }
337 |
338 | /**
339 | * Gets the version of the webview binary.
340 | */
341 | @instrument()
342 | async getVersion(): Promise {
343 | const result = await this.#send({ $type: "getVersion" });
344 | return returnResult(result, "string");
345 | }
346 |
347 | /**
348 | * Sets the size of the webview window.
349 | *
350 | * Note: this is the logical size of the window, not the physical size.
351 | * @see https://docs.rs/dpi/0.1.1/x86_64-unknown-linux-gnu/dpi/index.html#position-and-size-types
352 | */
353 | @instrument()
354 | async setSize(size: { width: number; height: number }): Promise {
355 | const result = await this.#send({ $type: "setSize", size });
356 | return returnAck(result);
357 | }
358 |
359 | /**
360 | * Gets the size of the webview window.
361 | *
362 | * Note: this is the logical size of the window, not the physical size.
363 | * @see https://docs.rs/dpi/0.1.1/x86_64-unknown-linux-gnu/dpi/index.html#position-and-size-types
364 | */
365 | @instrument()
366 | async getSize(
367 | includeDecorations?: boolean,
368 | ): Promise<{ width: number; height: number; scaleFactor: number }> {
369 | const result = await this.#send({
370 | $type: "getSize",
371 | include_decorations: includeDecorations,
372 | });
373 | return returnResult(
374 | result,
375 | "size",
376 | );
377 | }
378 |
379 | /**
380 | * Enters or exits fullscreen mode for the webview.
381 | *
382 | * @param fullscreen - If true, the webview will enter fullscreen mode. If false, the webview will exit fullscreen mode. If not specified, the webview will toggle fullscreen mode.
383 | */
384 | @instrument()
385 | async fullscreen(fullscreen?: boolean): Promise {
386 | const result = await this.#send({ $type: "fullscreen", fullscreen });
387 | return returnAck(result);
388 | }
389 |
390 | /**
391 | * Maximizes or unmaximizes the webview window.
392 | *
393 | * @param maximized - If true, the webview will be maximized. If false, the webview will be unmaximized. If not specified, the webview will toggle maximized state.
394 | */
395 | @instrument()
396 | async maximize(maximized?: boolean): Promise {
397 | const result = await this.#send({ $type: "maximize", maximized });
398 | return returnAck(result);
399 | }
400 |
401 | /**
402 | * Minimizes or unminimizes the webview window.
403 | *
404 | * @param minimized - If true, the webview will be minimized. If false, the webview will be unminimized. If not specified, the webview will toggle minimized state.
405 | */
406 | @instrument()
407 | async minimize(minimized?: boolean): Promise {
408 | const result = await this.#send({ $type: "minimize", minimized });
409 | return returnAck(result);
410 | }
411 |
412 | /**
413 | * Sets the title of the webview window.
414 | */
415 | @instrument()
416 | async setTitle(title: string): Promise {
417 | const result = await this.#send({
418 | $type: "setTitle",
419 | title,
420 | });
421 | return returnAck(result);
422 | }
423 |
424 | /**
425 | * Gets the title of the webview window.
426 | */
427 | @instrument()
428 | async getTitle(): Promise {
429 | const result = await this.#send({ $type: "getTitle" });
430 | return returnResult(result, "string");
431 | }
432 |
433 | /**
434 | * Sets the visibility of the webview window.
435 | */
436 | @instrument()
437 | async setVisibility(visible: boolean): Promise {
438 | const result = await this.#send({ $type: "setVisibility", visible });
439 | return returnAck(result);
440 | }
441 |
442 | /**
443 | * Returns true if the webview window is visible.
444 | */
445 | @instrument()
446 | async isVisible(): Promise {
447 | const result = await this.#send({ $type: "isVisible" });
448 | return returnResult(result, "boolean");
449 | }
450 |
451 | /**
452 | * Evaluates JavaScript code in the webview.
453 | */
454 | @instrument()
455 | async eval(code: string): Promise {
456 | const result = await this.#send({ $type: "eval", js: code });
457 | return returnAck(result);
458 | }
459 |
460 | /**
461 | * Opens the developer tools for the webview.
462 | */
463 | @instrument()
464 | async openDevTools(): Promise {
465 | const result = await this.#send({ $type: "openDevTools" });
466 | return returnAck(result);
467 | }
468 |
469 | /**
470 | * Reloads the webview with the provided html.
471 | */
472 | @instrument()
473 | async loadHtml(html: string): Promise {
474 | const result = await this.#send({ $type: "loadHtml", html });
475 | return returnAck(result);
476 | }
477 |
478 | /**
479 | * Loads a URL in the webview.
480 | */
481 | @instrument()
482 | async loadUrl(url: string, headers?: Record): Promise {
483 | const result = await this.#send({ $type: "loadUrl", url, headers });
484 | return returnAck(result);
485 | }
486 |
487 | /**
488 | * Destroys the webview and cleans up resources.
489 | *
490 | * Alternatively you can use the disposible interface.
491 | *
492 | * @example
493 | * ```ts
494 | * // The `using` keyword will automatically call `destroy` on the webview when
495 | * // the webview goes out of scope.
496 | * using webview = await createWebView({ title: "My Webview" });
497 | * ```
498 | */
499 | @instrument()
500 | destroy() {
501 | this[Symbol.dispose]();
502 | }
503 |
504 | /**
505 | * Part of the explicit resource management feature added in TS 5.2
506 | *
507 | * When a reference to the webview is stored with `using` this method
508 | * will be called automatically when the webview goes out of scope.
509 | *
510 | * @example
511 | *
512 | * ```ts
513 | * {
514 | * using webview = await createWebView({ title: "My Webview" });
515 | * } // Webview will be cleaned up here
516 | *
517 | * ```
518 | *
519 | * @see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#using-declarations-and-explicit-resource-management
520 | */
521 | [Symbol.dispose](): void {
522 | this.#internalEvent.removeAllListeners();
523 | this.#stdin.releaseLock();
524 | try {
525 | this.#process.kill();
526 | } catch (_) {
527 | _;
528 | }
529 | }
530 | }
531 |
--------------------------------------------------------------------------------
/src/clients/deno/schemas.ts:
--------------------------------------------------------------------------------
1 | // DO NOT EDIT: This file is auto-generated by generate-schema/index.ts
2 | import { z } from "npm:zod";
3 |
4 | /**
5 | * Messages that are sent unbidden from the webview to the client.
6 | */
7 | export type Notification =
8 | | {
9 | $type: "started";
10 | /** The version of the webview binary */
11 | version: string;
12 | }
13 | | {
14 | $type: "ipc";
15 | /** The message sent from the webview UI to the client. */
16 | message: string;
17 | }
18 | | {
19 | $type: "closed";
20 | };
21 |
22 | export type SizeWithScale = {
23 | /** The height of the window in logical pixels. */
24 | height: number;
25 | /** The ratio between physical and logical sizes. */
26 | scaleFactor: number;
27 | /** The width of the window in logical pixels. */
28 | width: number;
29 | };
30 |
31 | /**
32 | * Types that can be returned from webview results.
33 | */
34 | export type ResultType =
35 | | {
36 | $type: "string";
37 |
38 | value: string;
39 | }
40 | | {
41 | $type: "boolean";
42 |
43 | value: boolean;
44 | }
45 | | {
46 | $type: "float";
47 |
48 | value: number;
49 | }
50 | | {
51 | $type: "size";
52 |
53 | value: SizeWithScale;
54 | };
55 |
56 | /**
57 | * Responses from the webview to the client.
58 | */
59 | export type Response =
60 | | {
61 | $type: "ack";
62 |
63 | id: number;
64 | }
65 | | {
66 | $type: "result";
67 |
68 | id: number;
69 |
70 | result: ResultType;
71 | }
72 | | {
73 | $type: "err";
74 |
75 | id: number;
76 |
77 | message: string;
78 | };
79 |
80 | /**
81 | * Complete definition of all outbound messages from the webview to the client.
82 | */
83 | export type Message =
84 | | {
85 | $type: "notification";
86 |
87 | data: Notification;
88 | }
89 | | {
90 | $type: "response";
91 |
92 | data: Response;
93 | };
94 |
95 | export const Notification: z.ZodType = z.discriminatedUnion(
96 | "$type",
97 | [
98 | z.object({ $type: z.literal("started"), version: z.string() }),
99 | z.object({ $type: z.literal("ipc"), message: z.string() }),
100 | z.object({ $type: z.literal("closed") }),
101 | ],
102 | );
103 |
104 | export const SizeWithScale: z.ZodType = z.object({
105 | height: z.number(),
106 | scaleFactor: z.number(),
107 | width: z.number(),
108 | });
109 |
110 | export const ResultType: z.ZodType = z.discriminatedUnion("$type", [
111 | z.object({ $type: z.literal("string"), value: z.string() }),
112 | z.object({ $type: z.literal("boolean"), value: z.boolean() }),
113 | z.object({ $type: z.literal("float"), value: z.number() }),
114 | z.object({ $type: z.literal("size"), value: SizeWithScale }),
115 | ]);
116 |
117 | export const Response: z.ZodType = z.discriminatedUnion("$type", [
118 | z.object({ $type: z.literal("ack"), id: z.number().int() }),
119 | z.object({
120 | $type: z.literal("result"),
121 | id: z.number().int(),
122 | result: ResultType,
123 | }),
124 | z.object({
125 | $type: z.literal("err"),
126 | id: z.number().int(),
127 | message: z.string(),
128 | }),
129 | ]);
130 |
131 | export const Message: z.ZodType = z.discriminatedUnion("$type", [
132 | z.object({ $type: z.literal("notification"), data: Notification }),
133 | z.object({ $type: z.literal("response"), data: Response }),
134 | ]);
135 |
136 | /**
137 | * The content to load into the webview.
138 | */
139 | export type Content =
140 | | {
141 | /** Optional headers to send with the request. */
142 | headers?: Record;
143 | /** Url to load in the webview. Note: Don't use data URLs here, as they are not supported. Use the `html` field instead. */
144 | url: string;
145 | }
146 | | {
147 | /** Html to load in the webview. */
148 | html: string;
149 | /** What to set as the origin of the webview when loading html. */
150 | origin?: string;
151 | };
152 |
153 | export type Size = {
154 | /** The height of the window in logical pixels. */
155 | height: number;
156 | /** The width of the window in logical pixels. */
157 | width: number;
158 | };
159 |
160 | export type WindowSizeStates = "maximized" | "fullscreen";
161 | export type WindowSize = WindowSizeStates | Size;
162 |
163 | /**
164 | * Options for creating a webview.
165 | */
166 | export type Options = {
167 | /** Sets whether clicking an inactive window also clicks through to the webview. Default is false. */
168 | acceptFirstMouse?: boolean;
169 | /** When true, all media can be played without user interaction. Default is false. */
170 | autoplay?: boolean;
171 | /**
172 | * Enables clipboard access for the page rendered on Linux and Windows.
173 | *
174 | * macOS doesn’t provide such method and is always enabled by default. But your app will still need to add menu item accelerators to use the clipboard shortcuts.
175 | */
176 | clipboard?: boolean;
177 | /** When true, the window will have a border, a title bar, etc. Default is true. */
178 | decorations?: boolean;
179 | /**
180 | * Enable or disable webview devtools.
181 | *
182 | * Note this only enables devtools to the webview. To open it, you can call `webview.open_devtools()`, or right click the page and open it from the context menu.
183 | */
184 | devtools?: boolean;
185 | /** Sets whether the webview should be focused when created. Default is false. */
186 | focused?: boolean;
187 | /**
188 | * Run the WebView with incognito mode. Note that WebContext will be ingored if incognito is enabled.
189 | *
190 | * Platform-specific: - Windows: Requires WebView2 Runtime version 101.0.1210.39 or higher, does nothing on older versions, see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/archive?tabs=dotnetcsharp#10121039
191 | */
192 | incognito?: boolean;
193 | /** Run JavaScript code when loading new pages. When the webview loads a new page, this code will be executed. It is guaranteed that the code is executed before window.onload. */
194 | initializationScript?: string;
195 | /** Sets whether host should be able to receive messages from the webview via `window.ipc.postMessage`. */
196 | ipc?: boolean;
197 | /** The content to load into the webview. */
198 | load?: Content;
199 | /** The size of the window. */
200 | size?: WindowSize;
201 | /** Sets the title of the window. */
202 | title: string;
203 | /** Sets whether the window should be transparent. */
204 | transparent?: boolean;
205 | /** Sets the user agent to use when loading pages. */
206 | userAgent?: string;
207 | };
208 |
209 | export const Content: z.ZodType = z.union([
210 | z.object({
211 | headers: z.record(z.string(), z.string()).optional(),
212 | url: z.string(),
213 | }),
214 | z.object({ html: z.string(), origin: z.string() }),
215 | ]);
216 |
217 | export const Size: z.ZodType = z.object({
218 | height: z.number(),
219 | width: z.number(),
220 | });
221 |
222 | export const WindowSizeStates: z.ZodType = z.enum([
223 | "maximized",
224 | "fullscreen",
225 | ]);
226 | export const WindowSize: z.ZodType = z.union([
227 | WindowSizeStates,
228 | Size,
229 | ]);
230 |
231 | export const Options: z.ZodType = z.object({
232 | acceptFirstMouse: z.boolean().optional(),
233 | autoplay: z.boolean().optional(),
234 | clipboard: z.boolean().optional(),
235 | decorations: z.boolean().optional(),
236 | devtools: z.boolean().optional(),
237 | focused: z.boolean().optional(),
238 | incognito: z.boolean().optional(),
239 | initializationScript: z.string(),
240 | ipc: z.boolean().optional(),
241 | load: Content.optional(),
242 | size: WindowSize.optional(),
243 | title: z.string(),
244 | transparent: z.boolean().optional(),
245 | userAgent: z.string(),
246 | });
247 |
248 | /**
249 | * Explicit requests from the client to the webview.
250 | */
251 | export type Request =
252 | | {
253 | $type: "getVersion";
254 | /** The id of the request. */
255 | id: number;
256 | }
257 | | {
258 | $type: "eval";
259 | /** The id of the request. */
260 | id: number;
261 | /** The javascript to evaluate. */
262 | js: string;
263 | }
264 | | {
265 | $type: "setTitle";
266 | /** The id of the request. */
267 | id: number;
268 | /** The title to set. */
269 | title: string;
270 | }
271 | | {
272 | $type: "getTitle";
273 | /** The id of the request. */
274 | id: number;
275 | }
276 | | {
277 | $type: "setVisibility";
278 | /** The id of the request. */
279 | id: number;
280 | /** Whether the window should be visible or hidden. */
281 | visible: boolean;
282 | }
283 | | {
284 | $type: "isVisible";
285 | /** The id of the request. */
286 | id: number;
287 | }
288 | | {
289 | $type: "openDevTools";
290 | /** The id of the request. */
291 | id: number;
292 | }
293 | | {
294 | $type: "getSize";
295 | /** The id of the request. */
296 | id: number;
297 | /** Whether to include the title bar and borders in the size measurement. */
298 | include_decorations?: boolean;
299 | }
300 | | {
301 | $type: "setSize";
302 | /** The id of the request. */
303 | id: number;
304 | /** The size to set. */
305 | size: Size;
306 | }
307 | | {
308 | $type: "fullscreen";
309 | /** Whether to enter fullscreen mode. If left unspecified, the window will enter fullscreen mode if it is not already in fullscreen mode or exit fullscreen mode if it is currently in fullscreen mode. */
310 | fullscreen?: boolean;
311 | /** The id of the request. */
312 | id: number;
313 | }
314 | | {
315 | $type: "maximize";
316 | /** The id of the request. */
317 | id: number;
318 | /** Whether to maximize the window. If left unspecified, the window will be maximized if it is not already maximized or restored if it was previously maximized. */
319 | maximized?: boolean;
320 | }
321 | | {
322 | $type: "minimize";
323 | /** The id of the request. */
324 | id: number;
325 | /** Whether to minimize the window. If left unspecified, the window will be minimized if it is not already minimized or restored if it was previously minimized. */
326 | minimized?: boolean;
327 | }
328 | | {
329 | $type: "loadHtml";
330 | /** HTML to set as the content of the webview. */
331 | html: string;
332 | /** The id of the request. */
333 | id: number;
334 | /** What to set as the origin of the webview when loading html. If not specified, the origin will be set to the value of the `origin` field when the webview was created. */
335 | origin?: string;
336 | }
337 | | {
338 | $type: "loadUrl";
339 | /** Optional headers to send with the request. */
340 | headers?: Record;
341 | /** The id of the request. */
342 | id: number;
343 | /** URL to load in the webview. */
344 | url: string;
345 | };
346 |
347 | export const Request: z.ZodType = z.discriminatedUnion("$type", [
348 | z.object({ $type: z.literal("getVersion"), id: z.number().int() }),
349 | z.object({ $type: z.literal("eval"), id: z.number().int(), js: z.string() }),
350 | z.object({
351 | $type: z.literal("setTitle"),
352 | id: z.number().int(),
353 | title: z.string(),
354 | }),
355 | z.object({ $type: z.literal("getTitle"), id: z.number().int() }),
356 | z.object({
357 | $type: z.literal("setVisibility"),
358 | id: z.number().int(),
359 | visible: z.boolean(),
360 | }),
361 | z.object({ $type: z.literal("isVisible"), id: z.number().int() }),
362 | z.object({ $type: z.literal("openDevTools"), id: z.number().int() }),
363 | z.object({
364 | $type: z.literal("getSize"),
365 | id: z.number().int(),
366 | include_decorations: z.boolean().optional(),
367 | }),
368 | z.object({ $type: z.literal("setSize"), id: z.number().int(), size: Size }),
369 | z.object({
370 | $type: z.literal("fullscreen"),
371 | fullscreen: z.boolean().optional(),
372 | id: z.number().int(),
373 | }),
374 | z.object({
375 | $type: z.literal("maximize"),
376 | id: z.number().int(),
377 | maximized: z.boolean().optional(),
378 | }),
379 | z.object({
380 | $type: z.literal("minimize"),
381 | id: z.number().int(),
382 | minimized: z.boolean().optional(),
383 | }),
384 | z.object({
385 | $type: z.literal("loadHtml"),
386 | html: z.string(),
387 | id: z.number().int(),
388 | origin: z.string().optional(),
389 | }),
390 | z.object({
391 | $type: z.literal("loadUrl"),
392 | headers: z.record(z.string(), z.string()).optional(),
393 | id: z.number().int(),
394 | url: z.string(),
395 | }),
396 | ]);
397 |
--------------------------------------------------------------------------------
/src/clients/python/.python-version:
--------------------------------------------------------------------------------
1 | 3.13
2 |
--------------------------------------------------------------------------------
/src/clients/python/README.md:
--------------------------------------------------------------------------------
1 | # justbe-webview
2 |
3 | A light, cross-platform library for building web-based desktop apps with Python.
4 |
5 | ## Installation
6 |
7 | You can install justbe-webview using either `uv` (recommended) or `pip`:
8 |
9 | ```bash
10 | # Using uv (recommended)
11 | uv pip install justbe-webview
12 |
13 | # Using pip
14 | pip install justbe-webview
15 | ```
16 |
17 | ## Example
18 |
19 | ```python
20 | import asyncio
21 | from justbe_webview import (
22 | WebView,
23 | Options,
24 | ContentHtml,
25 | Notification,
26 | )
27 |
28 | async def main():
29 | config = Options(
30 | title="Simple",
31 | load=ContentHtml(html="Hello, World! "),
32 | )
33 |
34 | async with WebView(config) as webview:
35 | async def handle_start(event: Notification):
36 | await webview.eval("console.log('This is printed from eval!')")
37 |
38 | webview.on("started", handle_start)
39 |
40 | if __name__ == "__main__":
41 | asyncio.run(main())
42 | ```
43 |
44 | You can find more examples in the [examples directory](examples/), including:
45 | - Loading URLs
46 | - Loading HTML content
47 | - Window size management
48 | - IPC (Inter-Process Communication)
49 |
50 | ### Binary Management
51 |
52 | On first run, the client will:
53 | 1. Check for a cached binary in the user's cache directory
54 | 2. If not found, download the appropriate binary for the current platform
55 | 3. Cache the binary for future use
56 |
57 | The exact cache location depends on your operating system:
58 | - Linux: `~/.cache/justbe-webview/`
59 | - macOS: `~/Library/Caches/justbe-webview/`
60 | - Windows: `%LOCALAPPDATA%\justbe-webview\Cache\`
61 |
62 | ### Using a Custom Binary
63 |
64 | You can specify a custom binary path using the `WEBVIEW_BIN` environment variable:
65 |
66 | ```bash
67 | export WEBVIEW_BIN=/path/to/webview/binary
68 | python your_app.py
69 | ```
70 |
71 | When set, this will bypass the default binary resolution process.
72 |
--------------------------------------------------------------------------------
/src/clients/python/examples/ipc.py:
--------------------------------------------------------------------------------
1 | # /// script
2 | # requires-python = ">=3.13"
3 | # dependencies = [
4 | # "justbe-webview",
5 | # ]
6 | #
7 | # [tool.uv.sources]
8 | # justbe-webview = { path = "../" }
9 | # ///
10 | import asyncio
11 |
12 | from justbe_webview import WebView, Options, ContentHtml, IpcNotification
13 |
14 |
15 | async def main():
16 | print("Creating webview")
17 | config = Options(
18 | title="Simple",
19 | load=ContentHtml(
20 | html='Click me '
21 | ),
22 | ipc=True,
23 | )
24 |
25 | async with WebView(config) as webview:
26 |
27 | async def handle_ipc(event: IpcNotification):
28 | print(event.message)
29 |
30 | webview.on("ipc", handle_ipc)
31 |
32 | print("Webview closed")
33 |
34 |
35 | if __name__ == "__main__":
36 | asyncio.run(main())
37 |
--------------------------------------------------------------------------------
/src/clients/python/examples/load_html.py:
--------------------------------------------------------------------------------
1 | # /// script
2 | # requires-python = ">=3.13"
3 | # dependencies = [
4 | # "justbe-webview",
5 | # ]
6 | #
7 | # [tool.uv.sources]
8 | # justbe-webview = { path = "../" }
9 | # ///
10 | import asyncio
11 |
12 | from justbe_webview import (
13 | WebView,
14 | Options,
15 | ContentHtml,
16 | Notification,
17 | )
18 |
19 |
20 | async def main():
21 | print("Creating webview")
22 | config = Options(
23 | title="Load Html Example",
24 | load=ContentHtml(
25 | html="Initial html ",
26 | # Note: This origin is used with a custom protocol so it doesn't match
27 | # https://example.com. This doesn't need to be set, but can be useful if
28 | # you want to control resources that are scoped to a specific origin like
29 | # local storage or indexeddb.
30 | origin="example.com",
31 | ),
32 | devtools=True,
33 | )
34 |
35 | async with WebView(config) as webview:
36 |
37 | async def handle_start(event: Notification):
38 | await webview.open_devtools()
39 | await webview.load_html("Updated html! ")
40 |
41 | webview.on("started", handle_start)
42 |
43 | print("Webview closed")
44 |
45 |
46 | if __name__ == "__main__":
47 | asyncio.run(main())
48 |
--------------------------------------------------------------------------------
/src/clients/python/examples/load_url.py:
--------------------------------------------------------------------------------
1 | # /// script
2 | # requires-python = ">=3.13"
3 | # dependencies = [
4 | # "justbe-webview",
5 | # ]
6 | #
7 | # [tool.uv.sources]
8 | # justbe-webview = { path = "../" }
9 | # ///
10 | import asyncio
11 |
12 | from justbe_webview import (
13 | WebView,
14 | Options,
15 | ContentUrl,
16 | Notification,
17 | )
18 |
19 |
20 | async def main():
21 | print("Creating webview")
22 | config = Options(
23 | title="Load Url Example",
24 | load=ContentUrl(
25 | url="https://example.com",
26 | headers={
27 | "Content-Type": "text/html",
28 | },
29 | ),
30 | userAgent="curl/7.81.0",
31 | devtools=True,
32 | )
33 |
34 | async with WebView(config) as webview:
35 |
36 | async def handle_start(event: Notification):
37 | await webview.open_devtools()
38 | await asyncio.sleep(2) # Sleep for 2 seconds
39 | await webview.load_url(
40 | "https://val.town/",
41 | headers={
42 | "Content-Type": "text/html",
43 | },
44 | )
45 |
46 | webview.on("started", handle_start)
47 |
48 | print("Webview closed")
49 |
50 |
51 | if __name__ == "__main__":
52 | asyncio.run(main())
53 |
--------------------------------------------------------------------------------
/src/clients/python/examples/simple.py:
--------------------------------------------------------------------------------
1 | # /// script
2 | # requires-python = ">=3.13"
3 | # dependencies = [
4 | # "justbe-webview",
5 | # ]
6 | #
7 | # [tool.uv.sources]
8 | # justbe-webview = { path = "../" }
9 | # ///
10 | import asyncio
11 |
12 | from justbe_webview import (
13 | WebView,
14 | Options,
15 | ContentHtml,
16 | Notification,
17 | )
18 |
19 |
20 | async def main():
21 | print("Creating webview")
22 | config = Options(
23 | title="Simple",
24 | load=ContentHtml(html="Hello, World! "),
25 | devtools=True,
26 | initializationScript="console.log('This is printed from initializationScript!')",
27 | )
28 |
29 | async with WebView(config) as webview:
30 |
31 | async def handle_start(event: Notification):
32 | print("handle_start called")
33 | await webview.set_title("Title set from Python")
34 | current_title = await webview.get_title()
35 | print(f"Current title: {current_title}")
36 | await webview.open_devtools()
37 | await webview.eval("console.log('This is printed from eval!')")
38 |
39 | webview.on("started", handle_start)
40 |
41 | print("Webview closed")
42 |
43 |
44 | if __name__ == "__main__":
45 | asyncio.run(main())
46 |
--------------------------------------------------------------------------------
/src/clients/python/examples/window_size.py:
--------------------------------------------------------------------------------
1 | # /// script
2 | # requires-python = ">=3.13"
3 | # dependencies = [
4 | # "justbe-webview",
5 | # ]
6 | #
7 | # [tool.uv.sources]
8 | # justbe-webview = { path = "../" }
9 | # ///
10 | import asyncio
11 |
12 | from justbe_webview import WebView, Options, ContentHtml, IpcNotification, Size
13 |
14 |
15 | async def main():
16 | print("Creating webview")
17 | config = Options(
18 | title="Window Size",
19 | load=ContentHtml(
20 | html="""
21 | Window Sizes
22 |
23 | Maximize
24 | Minimize
25 | Fullscreen
26 |
27 | """
28 | ),
29 | size=Size(width=800, height=200),
30 | ipc=True,
31 | )
32 |
33 | async with WebView(config) as webview:
34 |
35 | async def handle_ipc(event: IpcNotification):
36 | message = event.message
37 | if message == "maximize":
38 | await webview.maximize()
39 | elif message == "minimize":
40 | await webview.minimize()
41 | elif message == "fullscreen":
42 | await webview.fullscreen()
43 | else:
44 | print(f"Unknown message: {message}")
45 |
46 | webview.on("ipc", handle_ipc)
47 |
48 | print("Webview closed")
49 |
50 |
51 | if __name__ == "__main__":
52 | asyncio.run(main())
53 |
--------------------------------------------------------------------------------
/src/clients/python/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "justbe-webview"
7 | version = "0.0.4"
8 | description = "A simple webview client"
9 | readme = "README.md"
10 | requires-python = ">=3.12"
11 | dependencies = [
12 | "aiofiles>=24.1.0",
13 | "msgspec>=0.19.0",
14 | "httpx>=0.27.2",
15 | "pyee>=12.0.0",
16 | "types-aiofiles>=24.1.0.20240626",
17 | "python-ulid>=3.0.0",
18 | ]
19 | [dependency-groups]
20 | dev = ["ruff>=0.6.9"]
21 |
--------------------------------------------------------------------------------
/src/clients/python/src/justbe_webview/__init__.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 | import platform
4 | import subprocess
5 | from typing import Any, Callable, Literal, Union, cast, TypeVar
6 | from pathlib import Path
7 | import aiofiles
8 | import httpx
9 | from pyee.asyncio import AsyncIOEventEmitter
10 | import msgspec
11 | import sys
12 |
13 | from .schemas import *
14 |
15 | # Import schemas
16 | from .schemas import (
17 | NotificationMessage,
18 | ResponseMessage,
19 | StartedNotification,
20 | ClosedNotification,
21 | StringResultType,
22 | BooleanResultType,
23 | SizeResultType,
24 | ResultType,
25 | AckResponse,
26 | ResultResponse,
27 | ErrResponse,
28 | Response as WebViewResponse,
29 | Notification as WebViewNotification,
30 | Message as WebViewMessage,
31 | Request as WebViewRequest,
32 | Options as WebViewOptions,
33 | GetVersionRequest,
34 | EvalRequest,
35 | SetTitleRequest,
36 | GetTitleRequest,
37 | SetVisibilityRequest,
38 | IsVisibleRequest,
39 | OpenDevToolsRequest,
40 | GetSizeRequest,
41 | SetSizeRequest,
42 | FullscreenRequest,
43 | MaximizeRequest,
44 | MinimizeRequest,
45 | LoadHtmlRequest,
46 | LoadUrlRequest,
47 | Size,
48 | )
49 |
50 | # Constants
51 | BIN_VERSION = "0.3.1"
52 |
53 | T = TypeVar("T", bound=WebViewNotification)
54 |
55 |
56 | def return_result(
57 | result: Union[AckResponse, ResultResponse, ErrResponse],
58 | expected_type: type[ResultType],
59 | ) -> Any:
60 | print(f"Return result: {result}")
61 | if isinstance(result, ResultResponse) and isinstance(result.result, expected_type):
62 | return result.result.value
63 | raise ValueError(f"Expected {expected_type.__name__} result got: {result}")
64 |
65 |
66 | def return_ack(result: Union[AckResponse, ResultResponse, ErrResponse]) -> None:
67 | print(f"Return ack: {result}")
68 | if isinstance(result, AckResponse):
69 | return
70 | if isinstance(result, ErrResponse):
71 | raise ValueError(result.message)
72 | raise ValueError(f"Unexpected response type: {type(result).__name__}")
73 |
74 |
75 | async def get_webview_bin(options: WebViewOptions) -> str:
76 | if "WEBVIEW_BIN" in os.environ:
77 | return os.environ["WEBVIEW_BIN"]
78 |
79 | flags = "-devtools" if cast(bool, getattr(options, "devtools", False)) else ""
80 | if (
81 | not flags
82 | and cast(bool, getattr(options, "transparent", False))
83 | and platform.system() == "Darwin"
84 | ):
85 | flags = "-transparent"
86 |
87 | cache_dir = get_cache_dir()
88 | file_name = f"webview-{BIN_VERSION}{flags}"
89 | if platform.system() == "Windows":
90 | file_name += ".exe"
91 | file_path = cache_dir / file_name
92 |
93 | if file_path.exists():
94 | return str(file_path)
95 |
96 | url = f"https://github.com/zephraph/webview/releases/download/webview-v{BIN_VERSION}/webview"
97 | if platform.system() == "Darwin":
98 | url += "-mac"
99 | if platform.machine() == "arm64":
100 | url += "-arm64"
101 | elif platform.system() == "Linux":
102 | url += "-linux"
103 | elif platform.system() == "Windows":
104 | url += "-windows"
105 | else:
106 | raise ValueError("Unsupported OS")
107 |
108 | url += flags
109 |
110 | if platform.system() == "Windows":
111 | url += ".exe"
112 |
113 | async with httpx.AsyncClient() as client:
114 | response = await client.get(url, follow_redirects=True)
115 | response.raise_for_status()
116 |
117 | cache_dir.mkdir(parents=True, exist_ok=True)
118 | async with aiofiles.open(file_path, "wb") as f:
119 | await f.write(response.content)
120 |
121 | os.chmod(file_path, 0o755)
122 | return str(file_path)
123 |
124 |
125 | def get_cache_dir() -> Path:
126 | if platform.system() == "Darwin":
127 | return Path.home() / "Library" / "Caches" / "python-webview"
128 |
129 | if platform.system() == "Linux":
130 | return Path.home() / ".cache" / "python-webview"
131 |
132 | if platform.system() == "Windows":
133 | return Path(os.environ["LOCALAPPDATA"]) / "python-webview" / "Cache"
134 |
135 | raise ValueError("Unsupported OS")
136 |
137 |
138 | class WebView:
139 | process: subprocess.Popen[bytes] | None = None
140 | __message_id = 0
141 |
142 | def __init__(self, options: WebViewOptions, webview_binary_path: str | None = None):
143 | self.options = options
144 | if webview_binary_path is not None:
145 | self.__start_process(webview_binary_path)
146 | self.internal_event = AsyncIOEventEmitter()
147 | self.external_event = AsyncIOEventEmitter()
148 | self.buffer = b""
149 |
150 | @property
151 | def message_id(self) -> int:
152 | current_id = self.__message_id
153 | self.__message_id += 1
154 | return current_id
155 |
156 | def __start_process(self, webview_binary_path: str):
157 | encoded_options = str(msgspec.json.encode(self.options), "utf-8")
158 | self.process = subprocess.Popen(
159 | [webview_binary_path, encoded_options],
160 | stdin=subprocess.PIPE,
161 | stdout=subprocess.PIPE,
162 | stderr=subprocess.PIPE, # Capture stderr properly
163 | text=False,
164 | bufsize=0,
165 | env=os.environ,
166 | )
167 | assert self.process.stdin is not None
168 | assert self.process.stdout is not None
169 | assert self.process.stderr is not None
170 |
171 | # Create StreamReader for non-blocking reads
172 | loop = asyncio.get_event_loop()
173 | self.stdout_reader = asyncio.StreamReader()
174 | protocol = asyncio.StreamReaderProtocol(self.stdout_reader)
175 | loop.create_task(loop.connect_read_pipe(lambda: protocol, self.process.stdout))
176 |
177 | # Also handle stderr
178 | self.stderr_reader = asyncio.StreamReader()
179 | stderr_protocol = asyncio.StreamReaderProtocol(self.stderr_reader)
180 | loop.create_task(
181 | loop.connect_read_pipe(lambda: stderr_protocol, self.process.stderr)
182 | )
183 | loop.create_task(self._pipe_stderr())
184 |
185 | async def _pipe_stderr(self):
186 | """Pipe stderr from the subprocess to Python's stderr"""
187 | while True:
188 | try:
189 | line = await self.stderr_reader.readline()
190 | if not line:
191 | break
192 | sys.stderr.buffer.write(line)
193 | sys.stderr.buffer.flush()
194 | except Exception:
195 | break
196 |
197 | async def __aenter__(self):
198 | bin_path = await get_webview_bin(self.options)
199 | if self.process is None:
200 | self.__start_process(bin_path)
201 | return self
202 |
203 | async def __aexit__(
204 | self,
205 | exc_type: type[BaseException] | None,
206 | exc_val: BaseException | None,
207 | exc_tb: Any | None,
208 | ) -> None:
209 | await self.wait_until_closed()
210 | self.destroy()
211 |
212 | async def send(self, request: WebViewRequest) -> WebViewResponse:
213 | if self.process is None:
214 | raise RuntimeError("Webview process not started")
215 | future: asyncio.Future[Union[AckResponse, ResultResponse, ErrResponse]] = (
216 | asyncio.Future()
217 | )
218 |
219 | def set_result(event: Union[AckResponse, ResultResponse, ErrResponse]) -> None:
220 | future.set_result(event)
221 |
222 | self.internal_event.once(str(request.id), set_result) # type: ignore
223 |
224 | assert self.process.stdin is not None
225 | encoded = msgspec.json.encode(request)
226 | self.process.stdin.write(encoded + b"\n")
227 | self.process.stdin.flush()
228 |
229 | result = await future
230 | return result
231 |
232 | async def recv(self) -> Union[WebViewNotification, None]:
233 | if self.process is None:
234 | raise RuntimeError("Webview process not started")
235 |
236 | print("Receiving messages from webview process...", flush=True)
237 |
238 | while True:
239 | try:
240 | # Non-blocking read using StreamReader
241 | chunk = await self.stdout_reader.read(8192)
242 | print(
243 | f"Read chunk size: {len(chunk) if chunk else 0} bytes", flush=True
244 | )
245 | if not chunk:
246 | print(
247 | "Received empty chunk, process may have closed stdout",
248 | flush=True,
249 | )
250 | return None
251 |
252 | self.buffer += chunk
253 | while b"\n" in self.buffer:
254 | message, self.buffer = self.buffer.split(b"\n", 1)
255 | print(f"Received raw message: {message}", flush=True)
256 | try:
257 | msg = msgspec.json.decode(message, type=WebViewMessage)
258 | print(f"Decoded message: {msg}", flush=True)
259 | if isinstance(msg, NotificationMessage):
260 | return msg.data
261 | elif isinstance(msg, ResponseMessage):
262 | self.internal_event.emit(str(msg.data.id), msg.data)
263 | except msgspec.DecodeError as e:
264 | print(f"Error parsing message: {message}", flush=True)
265 | print(f"Parse error details: {str(e)}", flush=True)
266 | except Exception as e:
267 | print(f"Error reading from stdout: {str(e)}", flush=True)
268 | return None
269 |
270 | async def process_message_loop(self):
271 | print("Processing message loop", flush=True)
272 | while True:
273 | notification = await self.recv()
274 | print(f"Received notification: {notification}", flush=True)
275 | if not notification:
276 | return
277 |
278 | if isinstance(notification, StartedNotification):
279 | version = notification.version
280 | if version != BIN_VERSION:
281 | print(
282 | f"Warning: Expected webview version {BIN_VERSION} but got {version}",
283 | flush=True,
284 | )
285 |
286 | tag = notification.__struct_config__.tag
287 | assert isinstance(tag, str)
288 | self.external_event.emit(tag, notification)
289 |
290 | if isinstance(notification, ClosedNotification):
291 | return
292 |
293 | async def wait_until_closed(self):
294 | await self.process_message_loop()
295 |
296 | def on(
297 | self,
298 | event: str,
299 | callback: Callable[[T], Any],
300 | ) -> None:
301 | if event == "ipc" and not getattr(self.options, "ipc", False):
302 | raise ValueError("IPC is not enabled for this webview")
303 |
304 | self.external_event.on(event, callback)
305 |
306 | def once(self, event: str, callback: Callable[[WebViewNotification], Any]):
307 | if event == "ipc" and not getattr(self.options, "ipc", False):
308 | raise ValueError("IPC is not enabled for this webview")
309 |
310 | self.external_event.once(event, callback) # type: ignore
311 |
312 | async def get_version(self) -> str:
313 | result = await self.send(GetVersionRequest(id=self.message_id))
314 | return return_result(result, StringResultType)
315 |
316 | async def set_size(self, size: dict[Literal["width", "height"], float]):
317 | result = await self.send(SetSizeRequest(id=self.message_id, size=Size(**size)))
318 | return_ack(result)
319 |
320 | async def get_size(
321 | self, include_decorations: bool = False
322 | ) -> dict[Literal["width", "height", "scaleFactor"], Union[int, float]]:
323 | result = await self.send(
324 | GetSizeRequest(id=self.message_id, include_decorations=include_decorations)
325 | )
326 | size_data = return_result(result, SizeResultType)
327 | return {
328 | "width": size_data.width,
329 | "height": size_data.height,
330 | "scaleFactor": size_data.scale_factor,
331 | }
332 |
333 | async def fullscreen(self, fullscreen: bool | None = None):
334 | result = await self.send(
335 | FullscreenRequest(id=self.message_id, fullscreen=fullscreen)
336 | )
337 | return_ack(result)
338 |
339 | async def maximize(self, maximized: bool | None = None):
340 | result = await self.send(
341 | MaximizeRequest(id=self.message_id, maximized=maximized)
342 | )
343 | return_ack(result)
344 |
345 | async def minimize(self, minimized: bool | None = None):
346 | result = await self.send(
347 | MinimizeRequest(id=self.message_id, minimized=minimized)
348 | )
349 | return_ack(result)
350 |
351 | async def set_title(self, title: str):
352 | result = await self.send(SetTitleRequest(id=self.message_id, title=title))
353 | return_ack(result)
354 |
355 | async def get_title(self) -> str:
356 | result = await self.send(GetTitleRequest(id=self.message_id))
357 | return return_result(result, StringResultType)
358 |
359 | async def set_visibility(self, visible: bool):
360 | result = await self.send(
361 | SetVisibilityRequest(id=self.message_id, visible=visible)
362 | )
363 | return_ack(result)
364 |
365 | async def is_visible(self) -> bool:
366 | result = await self.send(IsVisibleRequest(id=self.message_id))
367 | return return_result(result, BooleanResultType)
368 |
369 | async def eval(self, code: str):
370 | result = await self.send(EvalRequest(id=self.message_id, js=code))
371 | return_ack(result)
372 |
373 | async def open_devtools(self):
374 | result = await self.send(OpenDevToolsRequest(id=self.message_id))
375 | return_ack(result)
376 |
377 | async def load_html(self, html: str):
378 | result = await self.send(LoadHtmlRequest(id=self.message_id, html=html))
379 | return_ack(result)
380 |
381 | async def load_url(self, url: str, headers: dict[str, str] | None = None):
382 | result = await self.send(
383 | LoadUrlRequest(id=self.message_id, url=url, headers=headers)
384 | )
385 | return_ack(result)
386 |
387 | def destroy(self):
388 | if self.process is None:
389 | raise RuntimeError("Webview process not started")
390 | self.process.terminate()
391 | self.process.wait()
392 |
393 |
394 | async def create_webview(
395 | options: WebViewOptions,
396 | ) -> WebView:
397 | bin_path = await get_webview_bin(options)
398 | print(f"Created webview with bin path: {bin_path}")
399 | return WebView(options, bin_path)
400 |
--------------------------------------------------------------------------------
/src/clients/python/src/justbe_webview/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zephraph/webview/f237514c9565568849c3accbf46f8cc506103785/src/clients/python/src/justbe_webview/py.typed
--------------------------------------------------------------------------------
/src/clients/python/src/justbe_webview/schemas.py:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT: This file is auto-generated by generate-schema/index.ts
2 | from enum import Enum
3 | from typing import Union
4 | import msgspec
5 |
6 | __all__ = [
7 | "AckResponse",
8 | "BooleanResultType",
9 | "ClosedNotification",
10 | "Content",
11 | "ContentHtml",
12 | "ContentUrl",
13 | "ErrResponse",
14 | "EvalRequest",
15 | "FloatResultType",
16 | "FullscreenRequest",
17 | "GetSizeRequest",
18 | "GetTitleRequest",
19 | "GetVersionRequest",
20 | "IpcNotification",
21 | "IsVisibleRequest",
22 | "LoadHtmlRequest",
23 | "LoadUrlRequest",
24 | "MaximizeRequest",
25 | "Message",
26 | "MinimizeRequest",
27 | "Notification",
28 | "NotificationMessage",
29 | "OpenDevToolsRequest",
30 | "Options",
31 | "Request",
32 | "Response",
33 | "ResponseMessage",
34 | "ResultResponse",
35 | "ResultType",
36 | "SetSizeRequest",
37 | "SetTitleRequest",
38 | "SetVisibilityRequest",
39 | "Size",
40 | "SizeResultType",
41 | "SizeWithScale",
42 | "StartedNotification",
43 | "StringResultType",
44 | "WindowSize",
45 | "WindowSizeStates"
46 | ]
47 |
48 | class StartedNotification(msgspec.Struct, tag_field="$type", tag="started"):
49 | version: str
50 | """The version of the webview binary"""
51 |
52 | class IpcNotification(msgspec.Struct, tag_field="$type", tag="ipc"):
53 | message: str
54 | """The message sent from the webview UI to the client."""
55 |
56 | class ClosedNotification(msgspec.Struct, tag_field="$type", tag="closed"):
57 | pass
58 |
59 | Notification = Union[StartedNotification, IpcNotification, ClosedNotification]
60 | """
61 | Messages that are sent unbidden from the webview to the client.
62 | """
63 | class SizeWithScale(msgspec.Struct, omit_defaults=True):
64 | height: float
65 | """The height of the window in logical pixels."""
66 | scaleFactor: float
67 | """The ratio between physical and logical sizes."""
68 | width: float
69 | """The width of the window in logical pixels."""
70 |
71 | class StringResultType(msgspec.Struct, tag_field="$type", tag="string"):
72 | value: str
73 |
74 | class BooleanResultType(msgspec.Struct, tag_field="$type", tag="boolean"):
75 | value: bool
76 |
77 | class FloatResultType(msgspec.Struct, tag_field="$type", tag="float"):
78 | value: float
79 |
80 | class SizeResultType(msgspec.Struct, tag_field="$type", tag="size"):
81 | value: SizeWithScale
82 |
83 | ResultType = Union[StringResultType, BooleanResultType, FloatResultType, SizeResultType]
84 | """
85 | Types that can be returned from webview results.
86 | """
87 | class AckResponse(msgspec.Struct, tag_field="$type", tag="ack"):
88 | id: int
89 |
90 | class ResultResponse(msgspec.Struct, tag_field="$type", tag="result"):
91 | id: int
92 | result: ResultType
93 |
94 | class ErrResponse(msgspec.Struct, tag_field="$type", tag="err"):
95 | id: int
96 | message: str
97 |
98 | Response = Union[AckResponse, ResultResponse, ErrResponse]
99 | """
100 | Responses from the webview to the client.
101 | """
102 | class NotificationMessage(msgspec.Struct, tag_field="$type", tag="notification"):
103 | data: Notification
104 |
105 | class ResponseMessage(msgspec.Struct, tag_field="$type", tag="response"):
106 | data: Response
107 |
108 | Message = Union[NotificationMessage, ResponseMessage]
109 | """
110 | Complete definition of all outbound messages from the webview to the client.
111 | """
112 |
113 |
114 |
115 | class ContentUrl(msgspec.Struct, kw_only=True, omit_defaults=True):
116 | url: str
117 | """Url to load in the webview. Note: Don't use data URLs here, as they are not supported. Use the `html` field instead."""
118 | headers: Union[dict[str, str], None] = None
119 | """Optional headers to send with the request."""
120 |
121 | class ContentHtml(msgspec.Struct, kw_only=True, omit_defaults=True):
122 | html: str
123 | """Html to load in the webview."""
124 | origin: Union[str, None] = None
125 | """What to set as the origin of the webview when loading html."""
126 |
127 | Content = Union[ContentUrl, ContentHtml]
128 | """
129 | The content to load into the webview.
130 | """
131 | class Size(msgspec.Struct, omit_defaults=True):
132 | height: float
133 | """The height of the window in logical pixels."""
134 | width: float
135 | """The width of the window in logical pixels."""
136 |
137 | class WindowSizeStates(str, Enum):
138 | maximized = "maximized"
139 | fullscreen = "fullscreen"
140 |
141 | WindowSize = Union[WindowSizeStates, Size]
142 | class Options(msgspec.Struct, omit_defaults=True):
143 | """
144 | Options for creating a webview.
145 | """
146 | title: str
147 | """Sets the title of the window."""
148 | acceptFirstMouse: Union[bool, None] = None
149 | """Sets whether clicking an inactive window also clicks through to the webview. Default is false."""
150 | autoplay: Union[bool, None] = None
151 | """When true, all media can be played without user interaction. Default is false."""
152 | clipboard: Union[bool, None] = None
153 | """Enables clipboard access for the page rendered on Linux and Windows.
154 |
155 | macOS doesn’t provide such method and is always enabled by default. But your app will still need to add menu item accelerators to use the clipboard shortcuts."""
156 | decorations: Union[bool, None] = None
157 | """When true, the window will have a border, a title bar, etc. Default is true."""
158 | devtools: Union[bool, None] = None
159 | """Enable or disable webview devtools.
160 |
161 | Note this only enables devtools to the webview. To open it, you can call `webview.open_devtools()`, or right click the page and open it from the context menu."""
162 | focused: Union[bool, None] = None
163 | """Sets whether the webview should be focused when created. Default is false."""
164 | incognito: Union[bool, None] = None
165 | """Run the WebView with incognito mode. Note that WebContext will be ingored if incognito is enabled.
166 |
167 | Platform-specific: - Windows: Requires WebView2 Runtime version 101.0.1210.39 or higher, does nothing on older versions, see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/archive?tabs=dotnetcsharp#10121039"""
168 | initializationScript: Union[str, None] = None
169 | """Run JavaScript code when loading new pages. When the webview loads a new page, this code will be executed. It is guaranteed that the code is executed before window.onload."""
170 | ipc: Union[bool, None] = None
171 | """Sets whether host should be able to receive messages from the webview via `window.ipc.postMessage`."""
172 | load: Union[Content, None] = None
173 | """The content to load into the webview."""
174 | size: Union[WindowSize, None] = None
175 | """The size of the window."""
176 | transparent: Union[bool, None] = None
177 | """Sets whether the window should be transparent."""
178 | userAgent: Union[str, None] = None
179 | """Sets the user agent to use when loading pages."""
180 |
181 |
182 |
183 |
184 | class GetVersionRequest(msgspec.Struct, tag_field="$type", tag="getVersion"):
185 | id: int
186 | """The id of the request."""
187 |
188 | class EvalRequest(msgspec.Struct, tag_field="$type", tag="eval"):
189 | id: int
190 | """The id of the request."""
191 | js: str
192 | """The javascript to evaluate."""
193 |
194 | class SetTitleRequest(msgspec.Struct, tag_field="$type", tag="setTitle"):
195 | id: int
196 | """The id of the request."""
197 | title: str
198 | """The title to set."""
199 |
200 | class GetTitleRequest(msgspec.Struct, tag_field="$type", tag="getTitle"):
201 | id: int
202 | """The id of the request."""
203 |
204 | class SetVisibilityRequest(msgspec.Struct, tag_field="$type", tag="setVisibility"):
205 | id: int
206 | """The id of the request."""
207 | visible: bool
208 | """Whether the window should be visible or hidden."""
209 |
210 | class IsVisibleRequest(msgspec.Struct, tag_field="$type", tag="isVisible"):
211 | id: int
212 | """The id of the request."""
213 |
214 | class OpenDevToolsRequest(msgspec.Struct, tag_field="$type", tag="openDevTools"):
215 | id: int
216 | """The id of the request."""
217 |
218 | class GetSizeRequest(msgspec.Struct, tag_field="$type", tag="getSize"):
219 | id: int
220 | """The id of the request."""
221 | include_decorations: Union[bool, None] = None
222 | """Whether to include the title bar and borders in the size measurement."""
223 |
224 | class SetSizeRequest(msgspec.Struct, tag_field="$type", tag="setSize"):
225 | id: int
226 | """The id of the request."""
227 | size: Size
228 | """The size to set."""
229 |
230 | class FullscreenRequest(msgspec.Struct, tag_field="$type", tag="fullscreen"):
231 | id: int
232 | """The id of the request."""
233 | fullscreen: Union[bool, None] = None
234 | """Whether to enter fullscreen mode. If left unspecified, the window will enter fullscreen mode if it is not already in fullscreen mode or exit fullscreen mode if it is currently in fullscreen mode."""
235 |
236 | class MaximizeRequest(msgspec.Struct, tag_field="$type", tag="maximize"):
237 | id: int
238 | """The id of the request."""
239 | maximized: Union[bool, None] = None
240 | """Whether to maximize the window. If left unspecified, the window will be maximized if it is not already maximized or restored if it was previously maximized."""
241 |
242 | class MinimizeRequest(msgspec.Struct, tag_field="$type", tag="minimize"):
243 | id: int
244 | """The id of the request."""
245 | minimized: Union[bool, None] = None
246 | """Whether to minimize the window. If left unspecified, the window will be minimized if it is not already minimized or restored if it was previously minimized."""
247 |
248 | class LoadHtmlRequest(msgspec.Struct, tag_field="$type", tag="loadHtml"):
249 | html: str
250 | """HTML to set as the content of the webview."""
251 | id: int
252 | """The id of the request."""
253 | origin: Union[str, None] = None
254 | """What to set as the origin of the webview when loading html. If not specified, the origin will be set to the value of the `origin` field when the webview was created."""
255 |
256 | class LoadUrlRequest(msgspec.Struct, tag_field="$type", tag="loadUrl"):
257 | id: int
258 | """The id of the request."""
259 | url: str
260 | """URL to load in the webview."""
261 | headers: Union[dict[str, str], None] = None
262 | """Optional headers to send with the request."""
263 |
264 | Request = Union[GetVersionRequest, EvalRequest, SetTitleRequest, GetTitleRequest, SetVisibilityRequest, IsVisibleRequest, OpenDevToolsRequest, GetSizeRequest, SetSizeRequest, FullscreenRequest, MaximizeRequest, MinimizeRequest, LoadHtmlRequest, LoadUrlRequest]
265 | """
266 | Explicit requests from the client to the webview.
267 | """
268 |
269 |
270 |
271 |
--------------------------------------------------------------------------------
/src/clients/python/uv.lock:
--------------------------------------------------------------------------------
1 | version = 1
2 | revision = 1
3 | requires-python = ">=3.12"
4 |
5 | [[package]]
6 | name = "aiofiles"
7 | version = "24.1.0"
8 | source = { registry = "https://pypi.org/simple" }
9 | sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247 }
10 | wheels = [
11 | { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896 },
12 | ]
13 |
14 | [[package]]
15 | name = "anyio"
16 | version = "4.6.0"
17 | source = { registry = "https://pypi.org/simple" }
18 | dependencies = [
19 | { name = "idna" },
20 | { name = "sniffio" },
21 | ]
22 | sdist = { url = "https://files.pythonhosted.org/packages/78/49/f3f17ec11c4a91fe79275c426658e509b07547f874b14c1a526d86a83fc8/anyio-4.6.0.tar.gz", hash = "sha256:137b4559cbb034c477165047febb6ff83f390fc3b20bf181c1fc0a728cb8beeb", size = 170983 }
23 | wheels = [
24 | { url = "https://files.pythonhosted.org/packages/9e/ef/7a4f225581a0d7886ea28359179cb861d7fbcdefad29663fc1167b86f69f/anyio-4.6.0-py3-none-any.whl", hash = "sha256:c7d2e9d63e31599eeb636c8c5c03a7e108d73b345f064f1c19fdc87b79036a9a", size = 89631 },
25 | ]
26 |
27 | [[package]]
28 | name = "certifi"
29 | version = "2024.8.30"
30 | source = { registry = "https://pypi.org/simple" }
31 | sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 }
32 | wheels = [
33 | { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 },
34 | ]
35 |
36 | [[package]]
37 | name = "h11"
38 | version = "0.14.0"
39 | source = { registry = "https://pypi.org/simple" }
40 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
41 | wheels = [
42 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
43 | ]
44 |
45 | [[package]]
46 | name = "httpcore"
47 | version = "1.0.6"
48 | source = { registry = "https://pypi.org/simple" }
49 | dependencies = [
50 | { name = "certifi" },
51 | { name = "h11" },
52 | ]
53 | sdist = { url = "https://files.pythonhosted.org/packages/b6/44/ed0fa6a17845fb033bd885c03e842f08c1b9406c86a2e60ac1ae1b9206a6/httpcore-1.0.6.tar.gz", hash = "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f", size = 85180 }
54 | wheels = [
55 | { url = "https://files.pythonhosted.org/packages/06/89/b161908e2f51be56568184aeb4a880fd287178d176fd1c860d2217f41106/httpcore-1.0.6-py3-none-any.whl", hash = "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f", size = 78011 },
56 | ]
57 |
58 | [[package]]
59 | name = "httpx"
60 | version = "0.27.2"
61 | source = { registry = "https://pypi.org/simple" }
62 | dependencies = [
63 | { name = "anyio" },
64 | { name = "certifi" },
65 | { name = "httpcore" },
66 | { name = "idna" },
67 | { name = "sniffio" },
68 | ]
69 | sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189 }
70 | wheels = [
71 | { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 },
72 | ]
73 |
74 | [[package]]
75 | name = "idna"
76 | version = "3.10"
77 | source = { registry = "https://pypi.org/simple" }
78 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
79 | wheels = [
80 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
81 | ]
82 |
83 | [[package]]
84 | name = "justbe-webview"
85 | version = "0.0.1"
86 | source = { editable = "." }
87 | dependencies = [
88 | { name = "aiofiles" },
89 | { name = "httpx" },
90 | { name = "msgspec" },
91 | { name = "pyee" },
92 | { name = "python-ulid" },
93 | { name = "types-aiofiles" },
94 | ]
95 |
96 | [package.dev-dependencies]
97 | dev = [
98 | { name = "ruff" },
99 | ]
100 |
101 | [package.metadata]
102 | requires-dist = [
103 | { name = "aiofiles", specifier = ">=24.1.0" },
104 | { name = "httpx", specifier = ">=0.27.2" },
105 | { name = "msgspec", specifier = ">=0.19.0" },
106 | { name = "pyee", specifier = ">=12.0.0" },
107 | { name = "python-ulid", specifier = ">=3.0.0" },
108 | { name = "types-aiofiles", specifier = ">=24.1.0.20240626" },
109 | ]
110 |
111 | [package.metadata.requires-dev]
112 | dev = [{ name = "ruff", specifier = ">=0.6.9" }]
113 |
114 | [[package]]
115 | name = "msgspec"
116 | version = "0.19.0"
117 | source = { registry = "https://pypi.org/simple" }
118 | sdist = { url = "https://files.pythonhosted.org/packages/cf/9b/95d8ce458462b8b71b8a70fa94563b2498b89933689f3a7b8911edfae3d7/msgspec-0.19.0.tar.gz", hash = "sha256:604037e7cd475345848116e89c553aa9a233259733ab51986ac924ab1b976f8e", size = 216934 }
119 | wheels = [
120 | { url = "https://files.pythonhosted.org/packages/b2/5f/a70c24f075e3e7af2fae5414c7048b0e11389685b7f717bb55ba282a34a7/msgspec-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f98bd8962ad549c27d63845b50af3f53ec468b6318400c9f1adfe8b092d7b62f", size = 190485 },
121 | { url = "https://files.pythonhosted.org/packages/89/b0/1b9763938cfae12acf14b682fcf05c92855974d921a5a985ecc197d1c672/msgspec-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43bbb237feab761b815ed9df43b266114203f53596f9b6e6f00ebd79d178cdf2", size = 183910 },
122 | { url = "https://files.pythonhosted.org/packages/87/81/0c8c93f0b92c97e326b279795f9c5b956c5a97af28ca0fbb9fd86c83737a/msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cfc033c02c3e0aec52b71710d7f84cb3ca5eb407ab2ad23d75631153fdb1f12", size = 210633 },
123 | { url = "https://files.pythonhosted.org/packages/d0/ef/c5422ce8af73928d194a6606f8ae36e93a52fd5e8df5abd366903a5ca8da/msgspec-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d911c442571605e17658ca2b416fd8579c5050ac9adc5e00c2cb3126c97f73bc", size = 213594 },
124 | { url = "https://files.pythonhosted.org/packages/19/2b/4137bc2ed45660444842d042be2cf5b18aa06efd2cda107cff18253b9653/msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:757b501fa57e24896cf40a831442b19a864f56d253679f34f260dcb002524a6c", size = 214053 },
125 | { url = "https://files.pythonhosted.org/packages/9d/e6/8ad51bdc806aac1dc501e8fe43f759f9ed7284043d722b53323ea421c360/msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5f0f65f29b45e2816d8bded36e6b837a4bf5fb60ec4bc3c625fa2c6da4124537", size = 219081 },
126 | { url = "https://files.pythonhosted.org/packages/b1/ef/27dd35a7049c9a4f4211c6cd6a8c9db0a50647546f003a5867827ec45391/msgspec-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:067f0de1c33cfa0b6a8206562efdf6be5985b988b53dd244a8e06f993f27c8c0", size = 187467 },
127 | { url = "https://files.pythonhosted.org/packages/3c/cb/2842c312bbe618d8fefc8b9cedce37f773cdc8fa453306546dba2c21fd98/msgspec-0.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f12d30dd6266557aaaf0aa0f9580a9a8fbeadfa83699c487713e355ec5f0bd86", size = 190498 },
128 | { url = "https://files.pythonhosted.org/packages/58/95/c40b01b93465e1a5f3b6c7d91b10fb574818163740cc3acbe722d1e0e7e4/msgspec-0.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82b2c42c1b9ebc89e822e7e13bbe9d17ede0c23c187469fdd9505afd5a481314", size = 183950 },
129 | { url = "https://files.pythonhosted.org/packages/e8/f0/5b764e066ce9aba4b70d1db8b087ea66098c7c27d59b9dd8a3532774d48f/msgspec-0.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19746b50be214a54239aab822964f2ac81e38b0055cca94808359d779338c10e", size = 210647 },
130 | { url = "https://files.pythonhosted.org/packages/9d/87/bc14f49bc95c4cb0dd0a8c56028a67c014ee7e6818ccdce74a4862af259b/msgspec-0.19.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60ef4bdb0ec8e4ad62e5a1f95230c08efb1f64f32e6e8dd2ced685bcc73858b5", size = 213563 },
131 | { url = "https://files.pythonhosted.org/packages/53/2f/2b1c2b056894fbaa975f68f81e3014bb447516a8b010f1bed3fb0e016ed7/msgspec-0.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac7f7c377c122b649f7545810c6cd1b47586e3aa3059126ce3516ac7ccc6a6a9", size = 213996 },
132 | { url = "https://files.pythonhosted.org/packages/aa/5a/4cd408d90d1417e8d2ce6a22b98a6853c1b4d7cb7669153e4424d60087f6/msgspec-0.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5bc1472223a643f5ffb5bf46ccdede7f9795078194f14edd69e3aab7020d327", size = 219087 },
133 | { url = "https://files.pythonhosted.org/packages/23/d8/f15b40611c2d5753d1abb0ca0da0c75348daf1252220e5dda2867bd81062/msgspec-0.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:317050bc0f7739cb30d257ff09152ca309bf5a369854bbf1e57dffc310c1f20f", size = 187432 },
134 | ]
135 |
136 | [[package]]
137 | name = "pyee"
138 | version = "12.0.0"
139 | source = { registry = "https://pypi.org/simple" }
140 | dependencies = [
141 | { name = "typing-extensions" },
142 | ]
143 | sdist = { url = "https://files.pythonhosted.org/packages/d2/a7/8faaa62a488a2a1e0d56969757f087cbd2729e9bcfa508c230299f366b4c/pyee-12.0.0.tar.gz", hash = "sha256:c480603f4aa2927d4766eb41fa82793fe60a82cbfdb8d688e0d08c55a534e145", size = 29675 }
144 | wheels = [
145 | { url = "https://files.pythonhosted.org/packages/1d/0d/95993c08c721ec68892547f2117e8f9dfbcef2ca71e098533541b4a54d5f/pyee-12.0.0-py3-none-any.whl", hash = "sha256:7b14b74320600049ccc7d0e0b1becd3b4bd0a03c745758225e31a59f4095c990", size = 14831 },
146 | ]
147 |
148 | [[package]]
149 | name = "python-ulid"
150 | version = "3.0.0"
151 | source = { registry = "https://pypi.org/simple" }
152 | sdist = { url = "https://files.pythonhosted.org/packages/9a/db/e5e67aeca9c2420cb91f94007f30693cc3628ae9783a565fd33ffb3fbfdd/python_ulid-3.0.0.tar.gz", hash = "sha256:e50296a47dc8209d28629a22fc81ca26c00982c78934bd7766377ba37ea49a9f", size = 28822 }
153 | wheels = [
154 | { url = "https://files.pythonhosted.org/packages/63/4e/cc2ba2c0df2589f35a4db8473b8c2ba9bbfc4acdec4a94f1c78934d2350f/python_ulid-3.0.0-py3-none-any.whl", hash = "sha256:e4c4942ff50dbd79167ad01ac725ec58f924b4018025ce22c858bfcff99a5e31", size = 11194 },
155 | ]
156 |
157 | [[package]]
158 | name = "ruff"
159 | version = "0.6.9"
160 | source = { registry = "https://pypi.org/simple" }
161 | sdist = { url = "https://files.pythonhosted.org/packages/26/0d/6148a48dab5662ca1d5a93b7c0d13c03abd3cc7e2f35db08410e47cef15d/ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2", size = 3095355 }
162 | wheels = [
163 | { url = "https://files.pythonhosted.org/packages/6e/8f/f7a0a0ef1818662efb32ed6df16078c95da7a0a3248d64c2410c1e27799f/ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd", size = 10440526 },
164 | { url = "https://files.pythonhosted.org/packages/8b/69/b179a5faf936a9e2ab45bb412a668e4661eded964ccfa19d533f29463ef6/ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec", size = 10034612 },
165 | { url = "https://files.pythonhosted.org/packages/c7/ef/fd1b4be979c579d191eeac37b5cfc0ec906de72c8bcd8595e2c81bb700c1/ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c", size = 9706197 },
166 | { url = "https://files.pythonhosted.org/packages/29/61/b376d775deb5851cb48d893c568b511a6d3625ef2c129ad5698b64fb523c/ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e", size = 10751855 },
167 | { url = "https://files.pythonhosted.org/packages/13/d7/def9e5f446d75b9a9c19b24231a3a658c075d79163b08582e56fa5dcfa38/ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577", size = 10200889 },
168 | { url = "https://files.pythonhosted.org/packages/6c/d6/7f34160818bcb6e84ce293a5966cba368d9112ff0289b273fbb689046047/ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829", size = 11038678 },
169 | { url = "https://files.pythonhosted.org/packages/13/34/a40ff8ae62fb1b26fb8e6fa7e64bc0e0a834b47317880de22edd6bfb54fb/ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5", size = 11808682 },
170 | { url = "https://files.pythonhosted.org/packages/2e/6d/25a4386ae4009fc798bd10ba48c942d1b0b3e459b5403028f1214b6dd161/ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7", size = 11330446 },
171 | { url = "https://files.pythonhosted.org/packages/f7/f6/bdf891a9200d692c94ebcd06ae5a2fa5894e522f2c66c2a12dd5d8cb2654/ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f", size = 12483048 },
172 | { url = "https://files.pythonhosted.org/packages/a7/86/96f4252f41840e325b3fa6c48297e661abb9f564bd7dcc0572398c8daa42/ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa", size = 10936855 },
173 | { url = "https://files.pythonhosted.org/packages/45/87/801a52d26c8dbf73424238e9908b9ceac430d903c8ef35eab1b44fcfa2bd/ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb", size = 10713007 },
174 | { url = "https://files.pythonhosted.org/packages/be/27/6f7161d90320a389695e32b6ebdbfbedde28ccbf52451e4b723d7ce744ad/ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0", size = 10274594 },
175 | { url = "https://files.pythonhosted.org/packages/00/52/dc311775e7b5f5b19831563cb1572ecce63e62681bccc609867711fae317/ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625", size = 10608024 },
176 | { url = "https://files.pythonhosted.org/packages/98/b6/be0a1ddcbac65a30c985cf7224c4fce786ba2c51e7efeb5178fe410ed3cf/ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039", size = 10982085 },
177 | { url = "https://files.pythonhosted.org/packages/bb/a4/c84bc13d0b573cf7bb7d17b16d6d29f84267c92d79b2f478d4ce322e8e72/ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d", size = 8522088 },
178 | { url = "https://files.pythonhosted.org/packages/74/be/fc352bd8ca40daae8740b54c1c3e905a7efe470d420a268cd62150248c91/ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117", size = 9359275 },
179 | { url = "https://files.pythonhosted.org/packages/3e/14/fd026bc74ded05e2351681545a5f626e78ef831f8edce064d61acd2e6ec7/ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93", size = 8679879 },
180 | ]
181 |
182 | [[package]]
183 | name = "sniffio"
184 | version = "1.3.1"
185 | source = { registry = "https://pypi.org/simple" }
186 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
187 | wheels = [
188 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
189 | ]
190 |
191 | [[package]]
192 | name = "types-aiofiles"
193 | version = "24.1.0.20240626"
194 | source = { registry = "https://pypi.org/simple" }
195 | sdist = { url = "https://files.pythonhosted.org/packages/13/e9/013940b017c313c2e15c64017268fdb0c25e0638621fb8a5d9ebe00fb0f4/types-aiofiles-24.1.0.20240626.tar.gz", hash = "sha256:48604663e24bc2d5038eac05ccc33e75799b0779e93e13d6a8f711ddc306ac08", size = 9357 }
196 | wheels = [
197 | { url = "https://files.pythonhosted.org/packages/c3/ad/c4b3275d21c5be79487c4f6ed7cd13336997746fe099236cb29256a44a90/types_aiofiles-24.1.0.20240626-py3-none-any.whl", hash = "sha256:7939eca4a8b4f9c6491b6e8ef160caee9a21d32e18534a57d5ed90aee47c66b4", size = 9389 },
198 | ]
199 |
200 | [[package]]
201 | name = "typing-extensions"
202 | version = "4.12.2"
203 | source = { registry = "https://pypi.org/simple" }
204 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
205 | wheels = [
206 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
207 | ]
208 |
--------------------------------------------------------------------------------