Compare commits
9 Commits
wip/yaak-p
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22bc356b81 | ||
|
|
eb9b5b6bb6 | ||
|
|
b4a1c418bb | ||
|
|
45262edfbd | ||
|
|
aed7bd12ea | ||
|
|
b5928af1d7 | ||
|
|
6cc47bea38 | ||
|
|
b83d9e6765 | ||
|
|
c8ba35e268 |
@@ -8,7 +8,7 @@ Make Yaak runnable as a standalone CLI without Tauri as a dependency. The core R
|
||||
|
||||
```
|
||||
crates/ # Core crates - should NOT depend on Tauri
|
||||
crates-tauri/ # Tauri-specific crates (yaak-app-client, yaak-tauri-utils, etc.)
|
||||
crates-tauri/ # Tauri-specific crates (yaak-app, yaak-tauri-utils, etc.)
|
||||
crates-cli/ # CLI crate (yaak-cli)
|
||||
```
|
||||
|
||||
@@ -16,7 +16,7 @@ crates-cli/ # CLI crate (yaak-cli)
|
||||
|
||||
### 1. Folder Restructure
|
||||
|
||||
- Moved Tauri-dependent app code to `crates-tauri/yaak-app-client/`
|
||||
- Moved Tauri-dependent app code to `crates-tauri/yaak-app/`
|
||||
- Created `crates-tauri/yaak-tauri-utils/` for shared Tauri utilities (window traits, api_client, error handling)
|
||||
- Created `crates-cli/yaak-cli/` for the standalone CLI
|
||||
|
||||
@@ -50,14 +50,14 @@ crates-cli/ # CLI crate (yaak-cli)
|
||||
3. Move extension traits (e.g., `SomethingManagerExt`) to yaak-app or yaak-tauri-utils
|
||||
4. Initialize managers in yaak-app's `.setup()` block
|
||||
5. Remove `tauri` from Cargo.toml dependencies
|
||||
6. Update `crates-tauri/yaak-app-client/capabilities/default.json` to remove the plugin permission
|
||||
6. Update `crates-tauri/yaak-app/capabilities/default.json` to remove the plugin permission
|
||||
7. Replace `tauri::async_runtime::block_on` with `tokio::runtime::Handle::current().block_on()`
|
||||
|
||||
## Key Files
|
||||
|
||||
- `crates-tauri/yaak-app-client/src/lib.rs` - Main Tauri app, setup block initializes managers
|
||||
- `crates-tauri/yaak-app-client/src/commands.rs` - Migrated Tauri commands
|
||||
- `crates-tauri/yaak-app-client/src/models_ext.rs` - Database plugin and extension traits
|
||||
- `crates-tauri/yaak-app/src/lib.rs` - Main Tauri app, setup block initializes managers
|
||||
- `crates-tauri/yaak-app/src/commands.rs` - Migrated Tauri commands
|
||||
- `crates-tauri/yaak-app/src/models_ext.rs` - Database plugin and extension traits
|
||||
- `crates-tauri/yaak-tauri-utils/src/window.rs` - WorkspaceWindowTrait for window state
|
||||
- `crates/yaak-models/src/lib.rs` - Contains `init_standalone()` for CLI usage
|
||||
|
||||
@@ -79,5 +79,5 @@ e718a5f1 Refactor models_ext to use init_standalone from yaak-models
|
||||
## Testing
|
||||
|
||||
- Run `cargo check -p <crate>` to verify a crate builds without Tauri
|
||||
- Run `npm run client:dev` to test the Tauri app still works
|
||||
- Run `npm run app-dev` to test the Tauri app still works
|
||||
- Run `cargo run -p yaak-cli -- --help` to test the CLI
|
||||
|
||||
4
.gitattributes
vendored
@@ -1,5 +1,5 @@
|
||||
crates-tauri/yaak-app-client/vendored/**/* linguist-generated=true
|
||||
crates-tauri/yaak-app-client/gen/schemas/**/* linguist-generated=true
|
||||
crates-tauri/yaak-app/vendored/**/* linguist-generated=true
|
||||
crates-tauri/yaak-app/gen/schemas/**/* linguist-generated=true
|
||||
**/bindings/* linguist-generated=true
|
||||
crates/yaak-templates/pkg/* linguist-generated=true
|
||||
|
||||
|
||||
8
.github/workflows/release-app.yml
vendored
@@ -125,8 +125,8 @@ jobs:
|
||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||
|
||||
# Sign vendored binaries with hardened runtime and their specific entitlements
|
||||
codesign --force --options runtime --entitlements crates-tauri/yaak-app-client/macos/entitlements.yaakprotoc.plist --sign "$APPLE_SIGNING_IDENTITY" crates-tauri/yaak-app-client/vendored/protoc/yaakprotoc || true
|
||||
codesign --force --options runtime --entitlements crates-tauri/yaak-app-client/macos/entitlements.yaaknode.plist --sign "$APPLE_SIGNING_IDENTITY" crates-tauri/yaak-app-client/vendored/node/yaaknode || true
|
||||
codesign --force --options runtime --entitlements crates-tauri/yaak-app/macos/entitlements.yaakprotoc.plist --sign "$APPLE_SIGNING_IDENTITY" crates-tauri/yaak-app/vendored/protoc/yaakprotoc || true
|
||||
codesign --force --options runtime --entitlements crates-tauri/yaak-app/macos/entitlements.yaaknode.plist --sign "$APPLE_SIGNING_IDENTITY" crates-tauri/yaak-app/vendored/node/yaaknode || true
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
releaseBody: "[Changelog __VERSION__](https://yaak.app/blog/__VERSION__)"
|
||||
releaseDraft: true
|
||||
prerelease: true
|
||||
args: "${{ matrix.args }} --config ./crates-tauri/yaak-app-client/tauri.release.conf.json"
|
||||
args: "${{ matrix.args }} --config ./crates-tauri/yaak-app/tauri.release.conf.json"
|
||||
|
||||
# Build a per-machine NSIS installer for enterprise deployment (PDQ, SCCM, Intune)
|
||||
- name: Build and upload machine-wide installer (Windows only)
|
||||
@@ -171,7 +171,7 @@ jobs:
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
run: |
|
||||
Get-ChildItem -Recurse -Path target -File -Filter "*.exe.sig" | Remove-Item -Force
|
||||
npx tauri bundle ${{ matrix.args }} --bundles nsis --config ./crates-tauri/yaak-app-client/tauri.release.conf.json --config '{"bundle":{"createUpdaterArtifacts":true,"windows":{"nsis":{"installMode":"perMachine"}}}}'
|
||||
npx tauri bundle ${{ matrix.args }} --bundles nsis --config ./crates-tauri/yaak-app/tauri.release.conf.json --config '{"bundle":{"createUpdaterArtifacts":true,"windows":{"nsis":{"installMode":"perMachine"}}}}'
|
||||
$setup = Get-ChildItem -Recurse -Path target -Filter "*setup*.exe" | Select-Object -First 1
|
||||
$setupSig = "$($setup.FullName).sig"
|
||||
$dest = $setup.FullName -replace '-setup\.exe$', '-setup-machine.exe'
|
||||
|
||||
6
.github/workflows/release-cli-npm.yml
vendored
@@ -45,8 +45,8 @@ jobs:
|
||||
with:
|
||||
name: vendored-assets
|
||||
path: |
|
||||
crates-tauri/yaak-app-client/vendored/plugin-runtime/index.cjs
|
||||
crates-tauri/yaak-app-client/vendored/plugins
|
||||
crates-tauri/yaak-app/vendored/plugin-runtime/index.cjs
|
||||
crates-tauri/yaak-app/vendored/plugins
|
||||
if-no-files-found: error
|
||||
|
||||
build-binaries:
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: vendored-assets
|
||||
path: crates-tauri/yaak-app-client/vendored
|
||||
path: crates-tauri/yaak-app/vendored
|
||||
|
||||
- name: Set CLI build version
|
||||
shell: bash
|
||||
|
||||
3
.gitignore
vendored
@@ -39,8 +39,7 @@ codebook.toml
|
||||
target
|
||||
|
||||
# Per-worktree Tauri config (generated by post-checkout hook)
|
||||
crates-tauri/yaak-app-client/tauri.worktree.conf.json
|
||||
crates-tauri/yaak-app-proxy/tauri.worktree.conf.json
|
||||
crates-tauri/yaak-app/tauri.worktree.conf.json
|
||||
|
||||
# Tauri auto-generated permission files
|
||||
**/permissions/autogenerated
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
**/bindings/**
|
||||
**/routeTree.gen.ts
|
||||
crates/yaak-templates/pkg/**
|
||||
|
||||
153
Cargo.lock
generated
@@ -488,28 +488,6 @@ version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-rs"
|
||||
version = "1.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf"
|
||||
dependencies = [
|
||||
"aws-lc-sys",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-sys"
|
||||
version = "0.38.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cmake",
|
||||
"dunce",
|
||||
"fs_extra",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.7.9"
|
||||
@@ -2231,12 +2209,6 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs_extra"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
@@ -5160,16 +5132,6 @@ dependencies = [
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
@@ -6022,19 +5984,6 @@ dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rcgen"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2"
|
||||
dependencies = [
|
||||
"pem",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"time",
|
||||
"yasna",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.12"
|
||||
@@ -6786,8 +6735,6 @@ version = "0.23.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"log 0.4.29",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -6860,7 +6807,6 @@ version = "0.103.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
@@ -10299,7 +10245,7 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-app-client"
|
||||
name = "yaak-app"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"charset",
|
||||
@@ -10357,25 +10303,9 @@ dependencies = [
|
||||
"yaak-tauri-utils",
|
||||
"yaak-templates",
|
||||
"yaak-tls",
|
||||
"yaak-window",
|
||||
"yaak-ws",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-app-proxy"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"log 0.4.29",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-os",
|
||||
"yaak-mac-window",
|
||||
"yaak-proxy-lib",
|
||||
"yaak-rpc",
|
||||
"yaak-window",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-cli"
|
||||
version = "0.1.0"
|
||||
@@ -10445,25 +10375,6 @@ dependencies = [
|
||||
"yaak-models",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-database"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"include_dir",
|
||||
"log 0.4.29",
|
||||
"nanoid",
|
||||
"r2d2",
|
||||
"r2d2_sqlite",
|
||||
"rusqlite",
|
||||
"sea-query",
|
||||
"sea-query-rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
"ts-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-fonts"
|
||||
version = "0.1.0"
|
||||
@@ -10536,7 +10447,6 @@ dependencies = [
|
||||
"hyper-util",
|
||||
"log 0.4.29",
|
||||
"mime_guess",
|
||||
"native-tls",
|
||||
"regex 1.11.1",
|
||||
"reqwest",
|
||||
"serde",
|
||||
@@ -10606,7 +10516,6 @@ dependencies = [
|
||||
"thiserror 2.0.17",
|
||||
"ts-rs",
|
||||
"yaak-core",
|
||||
"yaak-database",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10638,52 +10547,6 @@ dependencies = [
|
||||
"zip-extract",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-proxy"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"pem",
|
||||
"rcgen",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-proxy-lib"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"include_dir",
|
||||
"log 0.4.29",
|
||||
"r2d2",
|
||||
"r2d2_sqlite",
|
||||
"rusqlite",
|
||||
"sea-query",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"ts-rs",
|
||||
"yaak-database",
|
||||
"yaak-proxy",
|
||||
"yaak-rpc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-rpc"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"log 0.4.29",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"ts-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-sse"
|
||||
version = "0.1.0"
|
||||
@@ -10749,17 +10612,6 @@ dependencies = [
|
||||
"yaak-models",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-window"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"log 0.4.29",
|
||||
"md5 0.8.0",
|
||||
"rand 0.9.1",
|
||||
"tauri",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yaak-ws"
|
||||
version = "0.1.0"
|
||||
@@ -10791,9 +10643,6 @@ name = "yasna"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
|
||||
dependencies = [
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
|
||||
19
Cargo.toml
@@ -2,9 +2,6 @@
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/yaak",
|
||||
# Common/foundation crates
|
||||
"crates/common/yaak-database",
|
||||
"crates/common/yaak-rpc",
|
||||
# Shared crates (no Tauri dependency)
|
||||
"crates/yaak-core",
|
||||
"crates/yaak-common",
|
||||
@@ -20,19 +17,14 @@ members = [
|
||||
"crates/yaak-tls",
|
||||
"crates/yaak-ws",
|
||||
"crates/yaak-api",
|
||||
"crates/yaak-proxy",
|
||||
# Proxy-specific crates
|
||||
"crates-proxy/yaak-proxy-lib",
|
||||
# CLI crates
|
||||
"crates-cli/yaak-cli",
|
||||
# Tauri-specific crates
|
||||
"crates-tauri/yaak-app-client",
|
||||
"crates-tauri/yaak-app-proxy",
|
||||
"crates-tauri/yaak-app",
|
||||
"crates-tauri/yaak-fonts",
|
||||
"crates-tauri/yaak-license",
|
||||
"crates-tauri/yaak-mac-window",
|
||||
"crates-tauri/yaak-tauri-utils",
|
||||
"crates-tauri/yaak-window",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
@@ -55,10 +47,6 @@ thiserror = "2.0.17"
|
||||
tokio = "1.48.0"
|
||||
ts-rs = "11.1.0"
|
||||
|
||||
# Internal crates - common/foundation
|
||||
yaak-database = { path = "crates/common/yaak-database" }
|
||||
yaak-rpc = { path = "crates/common/yaak-rpc" }
|
||||
|
||||
# Internal crates - shared
|
||||
yaak-core = { path = "crates/yaak-core" }
|
||||
yaak = { path = "crates/yaak" }
|
||||
@@ -75,17 +63,12 @@ yaak-templates = { path = "crates/yaak-templates" }
|
||||
yaak-tls = { path = "crates/yaak-tls" }
|
||||
yaak-ws = { path = "crates/yaak-ws" }
|
||||
yaak-api = { path = "crates/yaak-api" }
|
||||
yaak-proxy = { path = "crates/yaak-proxy" }
|
||||
|
||||
# Internal crates - proxy
|
||||
yaak-proxy-lib = { path = "crates-proxy/yaak-proxy-lib" }
|
||||
|
||||
# Internal crates - Tauri-specific
|
||||
yaak-fonts = { path = "crates-tauri/yaak-fonts" }
|
||||
yaak-license = { path = "crates-tauri/yaak-license" }
|
||||
yaak-mac-window = { path = "crates-tauri/yaak-mac-window" }
|
||||
yaak-tauri-utils = { path = "crates-tauri/yaak-tauri-utils" }
|
||||
yaak-window = { path = "crates-tauri/yaak-window" }
|
||||
|
||||
[profile.release]
|
||||
strip = false
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/JamesIves/github-sponsors-readme-action">
|
||||
<img width="200px" src="https://github.com/mountain-loop/yaak/raw/main/crates-tauri/yaak-app-client/icons/icon.png">
|
||||
<img width="200px" src="https://github.com/mountain-loop/yaak/raw/main/crates-tauri/yaak-app/icons/icon.png">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Button as BaseButton, type ButtonProps as BaseButtonProps } from "@yaakapp-internal/ui";
|
||||
import { forwardRef, useImperativeHandle, useRef } from "react";
|
||||
import type { HotkeyAction } from "../../hooks/useHotKey";
|
||||
import { useFormattedHotkey, useHotKey } from "../../hooks/useHotKey";
|
||||
|
||||
export type ButtonProps = BaseButtonProps & {
|
||||
hotkeyAction?: HotkeyAction;
|
||||
hotkeyLabelOnly?: boolean;
|
||||
hotkeyPriority?: number;
|
||||
};
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
||||
{ hotkeyAction, hotkeyPriority, hotkeyLabelOnly, title, ...props }: ButtonProps,
|
||||
ref,
|
||||
) {
|
||||
const hotkeyTrigger = useFormattedHotkey(hotkeyAction ?? null)?.join("");
|
||||
const fullTitle = hotkeyTrigger ? `${title ?? ""} ${hotkeyTrigger}`.trim() : title;
|
||||
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
useImperativeHandle<HTMLButtonElement | null, HTMLButtonElement | null>(
|
||||
ref,
|
||||
() => buttonRef.current,
|
||||
);
|
||||
|
||||
useHotKey(
|
||||
hotkeyAction ?? null,
|
||||
() => {
|
||||
buttonRef.current?.click();
|
||||
},
|
||||
{ priority: hotkeyPriority, enable: !hotkeyLabelOnly },
|
||||
);
|
||||
|
||||
return <BaseButton ref={buttonRef} title={fullTitle} {...props} />;
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
import {
|
||||
IconButton as BaseIconButton,
|
||||
type IconButtonProps as BaseIconButtonProps,
|
||||
} from "@yaakapp-internal/ui";
|
||||
import { forwardRef, useImperativeHandle, useRef } from "react";
|
||||
import type { HotkeyAction } from "../../hooks/useHotKey";
|
||||
import { useFormattedHotkey, useHotKey } from "../../hooks/useHotKey";
|
||||
|
||||
export type IconButtonProps = BaseIconButtonProps & {
|
||||
hotkeyAction?: HotkeyAction;
|
||||
hotkeyLabelOnly?: boolean;
|
||||
hotkeyPriority?: number;
|
||||
};
|
||||
|
||||
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(function IconButton(
|
||||
{ hotkeyAction, hotkeyPriority, hotkeyLabelOnly, title, ...props }: IconButtonProps,
|
||||
ref,
|
||||
) {
|
||||
const hotkeyTrigger = useFormattedHotkey(hotkeyAction ?? null)?.join("");
|
||||
const fullTitle = hotkeyTrigger ? `${title ?? ""} ${hotkeyTrigger}`.trim() : title;
|
||||
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
useImperativeHandle<HTMLButtonElement | null, HTMLButtonElement | null>(
|
||||
ref,
|
||||
() => buttonRef.current,
|
||||
);
|
||||
|
||||
useHotKey(
|
||||
hotkeyAction ?? null,
|
||||
() => {
|
||||
buttonRef.current?.click();
|
||||
},
|
||||
{ priority: hotkeyPriority, enable: !hotkeyLabelOnly },
|
||||
);
|
||||
|
||||
return <BaseIconButton ref={buttonRef} title={fullTitle} {...props} />;
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
export type { Appearance } from "@yaakapp-internal/theme";
|
||||
export {
|
||||
getCSSAppearance,
|
||||
getWindowAppearance,
|
||||
resolveAppearance,
|
||||
subscribeToPreferredAppearance,
|
||||
subscribeToWindowAppearanceChange,
|
||||
} from "@yaakapp-internal/theme";
|
||||
@@ -1,35 +0,0 @@
|
||||
import type { GetThemesResponse } from "@yaakapp-internal/plugins";
|
||||
import { defaultDarkTheme, defaultLightTheme } from "@yaakapp-internal/theme";
|
||||
import { invokeCmd } from "../tauri";
|
||||
import type { Appearance } from "./appearance";
|
||||
import { resolveAppearance } from "./appearance";
|
||||
|
||||
export { defaultDarkTheme, defaultLightTheme } from "@yaakapp-internal/theme";
|
||||
|
||||
export async function getThemes() {
|
||||
const themes = (await invokeCmd<GetThemesResponse[]>("cmd_get_themes")).flatMap((t) => t.themes);
|
||||
themes.sort((a, b) => a.label.localeCompare(b.label));
|
||||
// Remove duplicates, in case multiple plugins provide the same theme
|
||||
const uniqueThemes = Array.from(new Map(themes.map((t) => [t.id, t])).values());
|
||||
return { themes: [defaultDarkTheme, defaultLightTheme, ...uniqueThemes] };
|
||||
}
|
||||
|
||||
export async function getResolvedTheme(
|
||||
preferredAppearance: Appearance,
|
||||
appearanceSetting: string,
|
||||
themeLight: string,
|
||||
themeDark: string,
|
||||
) {
|
||||
const appearance = resolveAppearance(preferredAppearance, appearanceSetting);
|
||||
const { themes } = await getThemes();
|
||||
|
||||
const darkThemes = themes.filter((t) => t.dark);
|
||||
const lightThemes = themes.filter((t) => !t.dark);
|
||||
|
||||
const dark = darkThemes.find((t) => t.id === themeDark) ?? darkThemes[0] ?? defaultDarkTheme;
|
||||
const light = lightThemes.find((t) => t.id === themeLight) ?? lightThemes[0] ?? defaultLightTheme;
|
||||
|
||||
const active = appearance === "dark" ? dark : light;
|
||||
|
||||
return { dark, light, active };
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export type { YaakColorKey, YaakColors, YaakTheme } from "@yaakapp-internal/theme";
|
||||
export {
|
||||
addThemeStylesToDocument,
|
||||
applyThemeToDocument,
|
||||
completeTheme,
|
||||
getThemeCSS,
|
||||
indent,
|
||||
setThemeOnDocument,
|
||||
} from "@yaakapp-internal/theme";
|
||||
@@ -1 +0,0 @@
|
||||
export { YaakColor } from "@yaakapp-internal/theme";
|
||||
@@ -1,151 +0,0 @@
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file was automatically generated by TanStack Router.
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as WorkspacesIndexRouteImport } from './routes/workspaces/index'
|
||||
import { Route as WorkspacesWorkspaceIdIndexRouteImport } from './routes/workspaces/$workspaceId/index'
|
||||
import { Route as WorkspacesWorkspaceIdSettingsRouteImport } from './routes/workspaces/$workspaceId/settings'
|
||||
import { Route as WorkspacesWorkspaceIdRequestsRequestIdRouteImport } from './routes/workspaces/$workspaceId/requests/$requestId'
|
||||
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const WorkspacesIndexRoute = WorkspacesIndexRouteImport.update({
|
||||
id: '/workspaces/',
|
||||
path: '/workspaces/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const WorkspacesWorkspaceIdIndexRoute =
|
||||
WorkspacesWorkspaceIdIndexRouteImport.update({
|
||||
id: '/workspaces/$workspaceId/',
|
||||
path: '/workspaces/$workspaceId/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const WorkspacesWorkspaceIdSettingsRoute =
|
||||
WorkspacesWorkspaceIdSettingsRouteImport.update({
|
||||
id: '/workspaces/$workspaceId/settings',
|
||||
path: '/workspaces/$workspaceId/settings',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const WorkspacesWorkspaceIdRequestsRequestIdRoute =
|
||||
WorkspacesWorkspaceIdRequestsRequestIdRouteImport.update({
|
||||
id: '/workspaces/$workspaceId/requests/$requestId',
|
||||
path: '/workspaces/$workspaceId/requests/$requestId',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/workspaces': typeof WorkspacesIndexRoute
|
||||
'/workspaces/$workspaceId/settings': typeof WorkspacesWorkspaceIdSettingsRoute
|
||||
'/workspaces/$workspaceId': typeof WorkspacesWorkspaceIdIndexRoute
|
||||
'/workspaces/$workspaceId/requests/$requestId': typeof WorkspacesWorkspaceIdRequestsRequestIdRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/workspaces': typeof WorkspacesIndexRoute
|
||||
'/workspaces/$workspaceId/settings': typeof WorkspacesWorkspaceIdSettingsRoute
|
||||
'/workspaces/$workspaceId': typeof WorkspacesWorkspaceIdIndexRoute
|
||||
'/workspaces/$workspaceId/requests/$requestId': typeof WorkspacesWorkspaceIdRequestsRequestIdRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/workspaces/': typeof WorkspacesIndexRoute
|
||||
'/workspaces/$workspaceId/settings': typeof WorkspacesWorkspaceIdSettingsRoute
|
||||
'/workspaces/$workspaceId/': typeof WorkspacesWorkspaceIdIndexRoute
|
||||
'/workspaces/$workspaceId/requests/$requestId': typeof WorkspacesWorkspaceIdRequestsRequestIdRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/workspaces'
|
||||
| '/workspaces/$workspaceId/settings'
|
||||
| '/workspaces/$workspaceId'
|
||||
| '/workspaces/$workspaceId/requests/$requestId'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
| '/workspaces'
|
||||
| '/workspaces/$workspaceId/settings'
|
||||
| '/workspaces/$workspaceId'
|
||||
| '/workspaces/$workspaceId/requests/$requestId'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/workspaces/'
|
||||
| '/workspaces/$workspaceId/settings'
|
||||
| '/workspaces/$workspaceId/'
|
||||
| '/workspaces/$workspaceId/requests/$requestId'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
WorkspacesIndexRoute: typeof WorkspacesIndexRoute
|
||||
WorkspacesWorkspaceIdSettingsRoute: typeof WorkspacesWorkspaceIdSettingsRoute
|
||||
WorkspacesWorkspaceIdIndexRoute: typeof WorkspacesWorkspaceIdIndexRoute
|
||||
WorkspacesWorkspaceIdRequestsRequestIdRoute: typeof WorkspacesWorkspaceIdRequestsRequestIdRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/workspaces/': {
|
||||
id: '/workspaces/'
|
||||
path: '/workspaces'
|
||||
fullPath: '/workspaces'
|
||||
preLoaderRoute: typeof WorkspacesIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/workspaces/$workspaceId/': {
|
||||
id: '/workspaces/$workspaceId/'
|
||||
path: '/workspaces/$workspaceId'
|
||||
fullPath: '/workspaces/$workspaceId'
|
||||
preLoaderRoute: typeof WorkspacesWorkspaceIdIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/workspaces/$workspaceId/settings': {
|
||||
id: '/workspaces/$workspaceId/settings'
|
||||
path: '/workspaces/$workspaceId/settings'
|
||||
fullPath: '/workspaces/$workspaceId/settings'
|
||||
preLoaderRoute: typeof WorkspacesWorkspaceIdSettingsRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/workspaces/$workspaceId/requests/$requestId': {
|
||||
id: '/workspaces/$workspaceId/requests/$requestId'
|
||||
path: '/workspaces/$workspaceId/requests/$requestId'
|
||||
fullPath: '/workspaces/$workspaceId/requests/$requestId'
|
||||
preLoaderRoute: typeof WorkspacesWorkspaceIdRequestsRequestIdRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
WorkspacesIndexRoute: WorkspacesIndexRoute,
|
||||
WorkspacesWorkspaceIdSettingsRoute: WorkspacesWorkspaceIdSettingsRoute,
|
||||
WorkspacesWorkspaceIdIndexRoute: WorkspacesWorkspaceIdIndexRoute,
|
||||
WorkspacesWorkspaceIdRequestsRequestIdRoute:
|
||||
WorkspacesWorkspaceIdRequestsRequestIdRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
@@ -1,16 +0,0 @@
|
||||
const sharedConfig = require("@yaakapp-internal/tailwind-config");
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
...sharedConfig,
|
||||
content: [
|
||||
"./*.{html,ts,tsx}",
|
||||
"./commands/**/*.{ts,tsx}",
|
||||
"./components/**/*.{ts,tsx}",
|
||||
"./hooks/**/*.{ts,tsx}",
|
||||
"./init/**/*.{ts,tsx}",
|
||||
"./lib/**/*.{ts,tsx}",
|
||||
"./routes/**/*.{ts,tsx}",
|
||||
"../../packages/ui/src/**/*.{ts,tsx}",
|
||||
],
|
||||
};
|
||||
@@ -1,61 +0,0 @@
|
||||
// @ts-ignore
|
||||
import { tanstackRouter } from "@tanstack/router-plugin/vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { defineConfig, normalizePath } from "vite";
|
||||
import { viteStaticCopy } from "vite-plugin-static-copy";
|
||||
import svgr from "vite-plugin-svgr";
|
||||
import topLevelAwait from "vite-plugin-top-level-await";
|
||||
import wasm from "vite-plugin-wasm";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const cMapsDir = normalizePath(
|
||||
path.join(path.dirname(require.resolve("pdfjs-dist/package.json")), "cmaps"),
|
||||
);
|
||||
const standardFontsDir = normalizePath(
|
||||
path.join(path.dirname(require.resolve("pdfjs-dist/package.json")), "standard_fonts"),
|
||||
);
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(async () => {
|
||||
return {
|
||||
plugins: [
|
||||
wasm(),
|
||||
tanstackRouter({
|
||||
target: "react",
|
||||
routesDirectory: "./routes",
|
||||
generatedRouteTree: "./routeTree.gen.ts",
|
||||
autoCodeSplitting: true,
|
||||
}),
|
||||
svgr(),
|
||||
react(),
|
||||
topLevelAwait(),
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{ src: cMapsDir, dest: "" },
|
||||
{ src: standardFontsDir, dest: "" },
|
||||
],
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
sourcemap: true,
|
||||
outDir: "../../dist/apps/yaak-client",
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// Make chunk names readable
|
||||
chunkFileNames: "assets/chunk-[name]-[hash].js",
|
||||
entryFileNames: "assets/entry-[name]-[hash].js",
|
||||
assetFileNames: "assets/asset-[name]-[hash][extname]",
|
||||
},
|
||||
},
|
||||
},
|
||||
clearScreen: false,
|
||||
server: {
|
||||
port: parseInt(process.env.YAAK_CLIENT_DEV_PORT ?? process.env.YAAK_DEV_PORT ?? "1420", 10),
|
||||
strictPort: true,
|
||||
},
|
||||
envPrefix: ["VITE_", "TAURI_"],
|
||||
};
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
import type { ActionInvocation } from "@yaakapp-internal/proxy-lib";
|
||||
import { Button, type ButtonProps } from "@yaakapp-internal/ui";
|
||||
import { useCallback } from "react";
|
||||
import { useRpcMutation } from "../hooks/useRpcMutation";
|
||||
import { useActionMetadata } from "../hooks/useActionMetadata";
|
||||
|
||||
type ActionButtonProps = Omit<ButtonProps, "onClick" | "children"> & {
|
||||
action: ActionInvocation;
|
||||
/** Override the label from metadata */
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export function ActionButton({ action, children, ...props }: ActionButtonProps) {
|
||||
const meta = useActionMetadata(action);
|
||||
const { mutate, isPending } = useRpcMutation("execute_action");
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
mutate(action);
|
||||
}, [action, mutate]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
disabled={props.disabled || isPending}
|
||||
isLoading={isPending}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children ?? meta?.label ?? "…"}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { ActionInvocation } from "@yaakapp-internal/proxy-lib";
|
||||
import { IconButton, type IconButtonProps } from "@yaakapp-internal/ui";
|
||||
import { useCallback } from "react";
|
||||
import { useRpcMutation } from "../hooks/useRpcMutation";
|
||||
import { useActionMetadata } from "../hooks/useActionMetadata";
|
||||
|
||||
type ActionIconButtonProps = Omit<IconButtonProps, "onClick" | "title"> & {
|
||||
action: ActionInvocation;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export function ActionIconButton({ action, ...props }: ActionIconButtonProps) {
|
||||
const meta = useActionMetadata(action);
|
||||
const { mutate, isPending } = useRpcMutation("execute_action");
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
mutate(action);
|
||||
}, [action, mutate]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
{...props}
|
||||
title={props.title ?? meta?.label ?? "…"}
|
||||
disabled={props.disabled || isPending}
|
||||
isLoading={isPending}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import type { HttpExchange, ProxyHeader } from "@yaakapp-internal/proxy-lib";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
TruncatedWideTableCell,
|
||||
} from "@yaakapp-internal/ui";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface Props {
|
||||
exchanges: HttpExchange[];
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ExchangesTable({ exchanges, style, className }: Props) {
|
||||
if (exchanges.length === 0) {
|
||||
return <p className="text-text-subtlest text-sm">No traffic yet</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className} style={style}>
|
||||
<Table scrollable className="px-2">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Method</TableHeaderCell>
|
||||
<TableHeaderCell>URL</TableHeaderCell>
|
||||
<TableHeaderCell>Status</TableHeaderCell>
|
||||
<TableHeaderCell>Type</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{exchanges.map((ex) => (
|
||||
<TableRow key={ex.id}>
|
||||
<TableCell className="font-mono text-2xs">{ex.method}</TableCell>
|
||||
<TruncatedWideTableCell className="font-mono text-2xs">
|
||||
{ex.url}
|
||||
</TruncatedWideTableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={ex.resStatus} error={ex.error} />
|
||||
</TableCell>
|
||||
<TableCell className="text-text-subtle text-xs">
|
||||
{getContentType(ex.resHeaders)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusBadge({ status, error }: { status: number | null; error: string | null }) {
|
||||
if (error) return <span className="text-xs text-danger">Error</span>;
|
||||
if (status == null) return <span className="text-xs text-text-subtlest">—</span>;
|
||||
|
||||
const color =
|
||||
status >= 500
|
||||
? "text-danger"
|
||||
: status >= 400
|
||||
? "text-warning"
|
||||
: status >= 300
|
||||
? "text-notice"
|
||||
: "text-success";
|
||||
|
||||
return <span className={classNames("text-xs font-mono", color)}>{status}</span>;
|
||||
}
|
||||
|
||||
function getContentType(headers: ProxyHeader[]): string {
|
||||
const ct = headers.find((h) => h.name.toLowerCase() === "content-type")?.value;
|
||||
if (ct == null) return "—";
|
||||
// Strip parameters (e.g. "; charset=utf-8")
|
||||
return ct.split(";")[0]?.trim() ?? ct;
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
import { HeaderSize, IconButton, SidebarLayout, SplitLayout } from "@yaakapp-internal/ui";
|
||||
import classNames from "classnames";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useState } from "react";
|
||||
import { useLocalStorage } from "react-use";
|
||||
import { useRpcQueryWithEvent } from "../hooks/useRpcQueryWithEvent";
|
||||
import { getOsType } from "../lib/tauri";
|
||||
import { ActionIconButton } from "./ActionIconButton";
|
||||
import { ExchangesTable } from "./ExchangesTable";
|
||||
import { filteredExchangesAtom, Sidebar } from "./Sidebar";
|
||||
|
||||
export function ProxyLayout() {
|
||||
const os = getOsType();
|
||||
const exchanges = useAtomValue(filteredExchangesAtom);
|
||||
const [sidebarWidth, setSidebarWidth] = useLocalStorage("sidebar_width", 250);
|
||||
const [sidebarHidden, setSidebarHidden] = useLocalStorage("sidebar_hidden", false);
|
||||
const [floatingSidebarHidden, setFloatingSidebarHidden] = useLocalStorage(
|
||||
"floating_sidebar_hidden",
|
||||
true,
|
||||
);
|
||||
const [floating, setFloating] = useState(false);
|
||||
const { data: proxyState } = useRpcQueryWithEvent("get_proxy_state", {}, "proxy_state_changed");
|
||||
const isRunning = proxyState?.state === "running";
|
||||
const isHidden = floating ? (floatingSidebarHidden ?? true) : (sidebarHidden ?? false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"h-full w-full grid grid-rows-[auto_1fr]",
|
||||
os === "linux" && "border border-border-subtle",
|
||||
)}
|
||||
>
|
||||
<HeaderSize
|
||||
data-tauri-drag-region
|
||||
size="lg"
|
||||
osType={os}
|
||||
hideWindowControls={false}
|
||||
useNativeTitlebar={false}
|
||||
interfaceScale={1}
|
||||
className="x-theme-appHeader bg-surface"
|
||||
>
|
||||
<div className="grid grid-cols-[auto_minmax(0,1fr)_auto] items-center w-full h-full pointer-events-none">
|
||||
<div className="flex items-center pl-1 pointer-events-auto">
|
||||
<IconButton
|
||||
size="sm"
|
||||
title="Toggle sidebar"
|
||||
icon={isHidden ? "left_panel_hidden" : "left_panel_visible"}
|
||||
iconColor="secondary"
|
||||
onClick={() => {
|
||||
if (floating) {
|
||||
setFloatingSidebarHidden(!floatingSidebarHidden);
|
||||
} else {
|
||||
setSidebarHidden(!sidebarHidden);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="pointer-events-none flex items-center text-sm px-2"
|
||||
>
|
||||
Yaak Proxy
|
||||
</div>
|
||||
<div className="flex items-center gap-1 pr-1 pointer-events-auto">
|
||||
{isRunning ? (
|
||||
<>
|
||||
<span className="text-2xs text-success">Running :9090</span>
|
||||
<ActionIconButton
|
||||
action={{ scope: "global", action: "proxy_stop" }}
|
||||
icon="circle_stop"
|
||||
iconColor="secondary"
|
||||
size="sm"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<ActionIconButton
|
||||
action={{ scope: "global", action: "proxy_start" }}
|
||||
icon="circle_play"
|
||||
iconColor="secondary"
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</HeaderSize>
|
||||
<SidebarLayout
|
||||
width={sidebarWidth ?? 250}
|
||||
onWidthChange={setSidebarWidth}
|
||||
hidden={sidebarHidden ?? false}
|
||||
onHiddenChange={setSidebarHidden}
|
||||
floatingHidden={floatingSidebarHidden ?? true}
|
||||
onFloatingHiddenChange={setFloatingSidebarHidden}
|
||||
onFloatingChange={setFloating}
|
||||
sidebar={
|
||||
floating ? (
|
||||
<div
|
||||
className={classNames(
|
||||
"x-theme-sidebar",
|
||||
"h-full bg-surface border-r border-border-subtle",
|
||||
"grid grid-rows-[auto_1fr]",
|
||||
)}
|
||||
>
|
||||
<HeaderSize
|
||||
hideControls
|
||||
size="lg"
|
||||
className="border-transparent pl-1"
|
||||
osType={os}
|
||||
hideWindowControls={false}
|
||||
useNativeTitlebar={false}
|
||||
interfaceScale={1}
|
||||
>
|
||||
<IconButton
|
||||
size="sm"
|
||||
title="Toggle sidebar"
|
||||
icon="left_panel_visible"
|
||||
iconColor="secondary"
|
||||
onClick={() => setFloatingSidebarHidden(true)}
|
||||
/>
|
||||
</HeaderSize>
|
||||
<Sidebar />
|
||||
</div>
|
||||
) : (
|
||||
<Sidebar />
|
||||
)
|
||||
}
|
||||
>
|
||||
<SplitLayout
|
||||
storageKey="proxy_detail"
|
||||
layout="vertical"
|
||||
defaultRatio={0.4}
|
||||
firstSlot={({ style }) => (
|
||||
<ExchangesTable exchanges={exchanges} style={style} className="overflow-auto" />
|
||||
)}
|
||||
secondSlot={({ style }) => (
|
||||
<div
|
||||
style={style}
|
||||
className="p-3 text-text-subtlest text-sm border-t border-border-subtle"
|
||||
>
|
||||
Select a request to view details
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</SidebarLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
import type { HttpExchange } from "@yaakapp-internal/proxy-lib";
|
||||
import type { TreeNode } from "@yaakapp-internal/ui";
|
||||
import { selectedIdsFamily, Tree } from "@yaakapp-internal/ui";
|
||||
import { atom, useAtomValue } from "jotai";
|
||||
import { atomFamily } from "jotai/utils";
|
||||
import { useCallback } from "react";
|
||||
import { httpExchangesAtom } from "../lib/store";
|
||||
|
||||
/** A node in the sidebar tree — either a domain or a path segment. */
|
||||
export type SidebarItem = {
|
||||
id: string;
|
||||
label: string;
|
||||
exchangeIds: string[];
|
||||
};
|
||||
|
||||
const collapsedAtom = atomFamily((_treeId: string) => atom<Record<string, boolean>>({}));
|
||||
|
||||
export const SIDEBAR_TREE_ID = "proxy-sidebar";
|
||||
|
||||
const sidebarTreeAtom = atom<TreeNode<SidebarItem>>((get) => {
|
||||
const exchanges = get(httpExchangesAtom);
|
||||
return buildTree(exchanges);
|
||||
});
|
||||
|
||||
/** Exchanges filtered by the currently selected sidebar node(s). */
|
||||
export const filteredExchangesAtom = atom((get) => {
|
||||
const exchanges = get(httpExchangesAtom);
|
||||
const tree = get(sidebarTreeAtom);
|
||||
const selectedIds = get(selectedIdsFamily(SIDEBAR_TREE_ID));
|
||||
|
||||
// Nothing selected or root selected → show all
|
||||
if (selectedIds.length === 0 || selectedIds.includes("root")) {
|
||||
return exchanges;
|
||||
}
|
||||
|
||||
// Collect exchange IDs from all selected nodes
|
||||
const allowedIds = new Set<string>();
|
||||
const nodeMap = new Map<string, SidebarItem>();
|
||||
collectNodes(tree, nodeMap);
|
||||
|
||||
for (const selectedId of selectedIds) {
|
||||
const node = nodeMap.get(selectedId);
|
||||
if (node) {
|
||||
for (const id of node.exchangeIds) {
|
||||
allowedIds.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return exchanges.filter((ex) => allowedIds.has(ex.id));
|
||||
});
|
||||
|
||||
function collectNodes(node: TreeNode<SidebarItem>, map: Map<string, SidebarItem>) {
|
||||
map.set(node.item.id, node.item);
|
||||
for (const child of node.children ?? []) {
|
||||
collectNodes(child, map);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a domain → path-segment trie from a flat list of exchanges.
|
||||
*
|
||||
* Example: Given URLs
|
||||
* GET https://api.example.com/v1/users
|
||||
* GET https://api.example.com/v1/users/123
|
||||
* POST https://api.example.com/v1/orders
|
||||
*
|
||||
* Produces:
|
||||
* api.example.com
|
||||
* /v1
|
||||
* /users
|
||||
* /123
|
||||
* /orders
|
||||
*/
|
||||
function buildTree(exchanges: HttpExchange[]): TreeNode<SidebarItem> {
|
||||
const root: SidebarItem = { id: "root", label: "All Traffic", exchangeIds: [] };
|
||||
const rootNode: TreeNode<SidebarItem> = {
|
||||
item: root,
|
||||
parent: null,
|
||||
depth: 0,
|
||||
children: [],
|
||||
draggable: false,
|
||||
};
|
||||
|
||||
// Intermediate trie structure for building
|
||||
type TrieNode = {
|
||||
id: string;
|
||||
label: string;
|
||||
exchangeIds: string[];
|
||||
children: Map<string, TrieNode>;
|
||||
};
|
||||
|
||||
const domainMap = new Map<string, TrieNode>();
|
||||
|
||||
for (const ex of exchanges) {
|
||||
let hostname: string;
|
||||
let segments: string[];
|
||||
try {
|
||||
const url = new URL(ex.url);
|
||||
hostname = url.host;
|
||||
segments = url.pathname.split("/").filter(Boolean);
|
||||
} catch {
|
||||
hostname = ex.url;
|
||||
segments = [];
|
||||
}
|
||||
|
||||
// Get or create domain node
|
||||
let domainNode = domainMap.get(hostname);
|
||||
if (!domainNode) {
|
||||
domainNode = {
|
||||
id: `domain:${hostname}`,
|
||||
label: hostname,
|
||||
exchangeIds: [],
|
||||
children: new Map(),
|
||||
};
|
||||
domainMap.set(hostname, domainNode);
|
||||
}
|
||||
domainNode.exchangeIds.push(ex.id);
|
||||
|
||||
// Walk path segments
|
||||
let current = domainNode;
|
||||
const pathSoFar: string[] = [];
|
||||
for (const seg of segments) {
|
||||
pathSoFar.push(seg);
|
||||
let child = current.children.get(seg);
|
||||
if (!child) {
|
||||
child = {
|
||||
id: `path:${hostname}/${pathSoFar.join("/")}`,
|
||||
label: `/${seg}`,
|
||||
exchangeIds: [],
|
||||
children: new Map(),
|
||||
};
|
||||
current.children.set(seg, child);
|
||||
}
|
||||
child.exchangeIds.push(ex.id);
|
||||
current = child;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert trie to TreeNode structure
|
||||
function toTreeNode(
|
||||
trie: TrieNode,
|
||||
parent: TreeNode<SidebarItem>,
|
||||
depth: number,
|
||||
): TreeNode<SidebarItem> {
|
||||
const node: TreeNode<SidebarItem> = {
|
||||
item: {
|
||||
id: trie.id,
|
||||
label: trie.label,
|
||||
exchangeIds: trie.exchangeIds,
|
||||
},
|
||||
parent,
|
||||
depth,
|
||||
children: [],
|
||||
draggable: false,
|
||||
};
|
||||
const sortedChildren = [...trie.children.values()].sort((a, b) =>
|
||||
a.label.localeCompare(b.label),
|
||||
);
|
||||
for (const child of sortedChildren) {
|
||||
node.children?.push(toTreeNode(child, node, depth + 1));
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
// Add a "Domains" folder between root and domain nodes
|
||||
const allExchangeIds = exchanges.map((ex) => ex.id);
|
||||
const domainsFolder: TreeNode<SidebarItem> = {
|
||||
item: { id: "domains", label: "Domains", exchangeIds: allExchangeIds },
|
||||
parent: rootNode,
|
||||
depth: 1,
|
||||
children: [],
|
||||
draggable: false,
|
||||
};
|
||||
|
||||
const sortedDomains = [...domainMap.values()].sort((a, b) => a.label.localeCompare(b.label));
|
||||
for (const domain of sortedDomains) {
|
||||
domainsFolder.children?.push(toTreeNode(domain, domainsFolder, 2));
|
||||
}
|
||||
|
||||
rootNode.children?.push(domainsFolder);
|
||||
|
||||
return rootNode;
|
||||
}
|
||||
|
||||
function ItemInner({ item }: { item: SidebarItem }) {
|
||||
const count = item.exchangeIds.length;
|
||||
return (
|
||||
<div className="flex items-center gap-2 w-full min-w-0">
|
||||
<span className="truncate">{item.label}</span>
|
||||
{count > 0 && <span className="ml-auto text-text-subtlest text-2xs shrink-0">{count}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Sidebar() {
|
||||
const tree = useAtomValue(sidebarTreeAtom);
|
||||
const treeId = SIDEBAR_TREE_ID;
|
||||
|
||||
const getItemKey = useCallback(
|
||||
(item: SidebarItem) => `${item.id}:${item.exchangeIds.length}`,
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<aside className="x-theme-sidebar bg-surface h-full w-full min-w-0 overflow-y-auto border-r border-border-subtle">
|
||||
<div className="pt-2 text-xs">
|
||||
<Tree
|
||||
treeId={treeId}
|
||||
collapsedAtom={collapsedAtom(treeId)}
|
||||
className="px-2 pb-10"
|
||||
root={tree}
|
||||
getItemKey={getItemKey}
|
||||
ItemInner={ItemInner}
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
// Hardcode font size for now. In the future, this could be configurable.
|
||||
document.documentElement.style.fontSize = "15px";
|
||||
@@ -1,32 +0,0 @@
|
||||
import type { ActionInvocation, ActionMetadata } from "@yaakapp-internal/proxy-lib";
|
||||
import { useEffect, useState } from "react";
|
||||
import { rpc } from "../lib/rpc";
|
||||
|
||||
/** Look up metadata for a specific action invocation. */
|
||||
// oxlint-disable-next-line no-redundant-type-constituents -- ActionMetadata resolves at runtime
|
||||
export function useActionMetadata(action: ActionInvocation): ActionMetadata | null {
|
||||
// oxlint-disable-next-line no-redundant-type-constituents -- ActionMetadata resolves at runtime
|
||||
const [meta, setMeta] = useState<ActionMetadata | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
void getActions().then((actions) => {
|
||||
const match = actions.find(
|
||||
([inv]) => inv.scope === action.scope && inv.action === action.action,
|
||||
);
|
||||
setMeta(match?.[1] ?? null);
|
||||
});
|
||||
}, [action]);
|
||||
|
||||
return meta;
|
||||
}
|
||||
|
||||
let cachedActions: [ActionInvocation, ActionMetadata][] | null = null;
|
||||
|
||||
/** Fetch and cache all action metadata. */
|
||||
async function getActions(): Promise<[ActionInvocation, ActionMetadata][]> {
|
||||
if (!cachedActions) {
|
||||
const { actions } = await rpc("list_actions", {});
|
||||
cachedActions = actions;
|
||||
}
|
||||
return cachedActions;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { RpcEventSchema } from "@yaakapp-internal/proxy-lib";
|
||||
import { useEffect } from "react";
|
||||
import { listen } from "../lib/rpc";
|
||||
|
||||
/**
|
||||
* Subscribe to an RPC event. Cleans up automatically on unmount.
|
||||
*/
|
||||
export function useRpcEvent<K extends keyof RpcEventSchema>(
|
||||
event: K & string,
|
||||
callback: (payload: RpcEventSchema[K]) => void,
|
||||
) {
|
||||
useEffect(() => {
|
||||
return listen(event, callback);
|
||||
}, [event, callback]);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { type UseMutationOptions, useMutation } from "@tanstack/react-query";
|
||||
import type { RpcSchema } from "@yaakapp-internal/proxy-lib";
|
||||
import { minPromiseMillis } from "@yaakapp-internal/ui";
|
||||
import type { Req, Res } from "../lib/rpc";
|
||||
import { rpc } from "../lib/rpc";
|
||||
|
||||
/**
|
||||
* React Query mutation wrapper for RPC commands.
|
||||
*/
|
||||
export function useRpcMutation<K extends keyof RpcSchema>(
|
||||
cmd: K,
|
||||
opts?: Omit<UseMutationOptions<Res<K>, Error, Req<K>>, "mutationFn">,
|
||||
) {
|
||||
return useMutation<Res<K>, Error, Req<K>>({
|
||||
mutationFn: (payload) => minPromiseMillis(rpc(cmd, payload)),
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
|
||||
import type { RpcSchema } from "@yaakapp-internal/proxy-lib";
|
||||
import type { Req, Res } from "../lib/rpc";
|
||||
import { rpc } from "../lib/rpc";
|
||||
|
||||
/**
|
||||
* React Query wrapper for RPC commands.
|
||||
* Automatically caches by [cmd, payload] and supports all useQuery options.
|
||||
*/
|
||||
export function useRpcQuery<K extends keyof RpcSchema>(
|
||||
cmd: K,
|
||||
payload: Req<K>,
|
||||
opts?: Omit<UseQueryOptions<Res<K>>, "queryKey" | "queryFn">,
|
||||
) {
|
||||
return useQuery<Res<K>>({
|
||||
queryKey: [cmd, payload],
|
||||
queryFn: () => rpc(cmd, payload),
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { type UseQueryOptions, useQueryClient } from "@tanstack/react-query";
|
||||
import type { RpcEventSchema, RpcSchema } from "@yaakapp-internal/proxy-lib";
|
||||
import type { Req, Res } from "../lib/rpc";
|
||||
import { useRpcEvent } from "./useRpcEvent";
|
||||
import { useRpcQuery } from "./useRpcQuery";
|
||||
|
||||
/**
|
||||
* Combines useRpcQuery with an event listener that invalidates the query
|
||||
* whenever the specified event fires, keeping data fresh automatically.
|
||||
*/
|
||||
export function useRpcQueryWithEvent<
|
||||
K extends keyof RpcSchema,
|
||||
E extends keyof RpcEventSchema,
|
||||
>(cmd: K, payload: Req<K>, event: E, opts?: Omit<UseQueryOptions<Res<K>>, "queryKey" | "queryFn">) {
|
||||
const queryClient = useQueryClient();
|
||||
const query = useRpcQuery(cmd, payload, opts);
|
||||
|
||||
useRpcEvent(event, () => {
|
||||
void queryClient.invalidateQueries({ queryKey: [cmd, payload] });
|
||||
});
|
||||
|
||||
return query;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Yaak Proxy</title>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html,
|
||||
body {
|
||||
background-color: #1b1a29;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="text-base">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/font-size.ts"></script>
|
||||
<script type="module" src="/lib/theme.ts"></script>
|
||||
<script type="module" src="/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,5 +0,0 @@
|
||||
export function fireAndForget(promise: Promise<unknown>) {
|
||||
promise.catch((err: unknown) => {
|
||||
console.error("Unhandled async error:", err);
|
||||
});
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import type { ActionInvocation, ActionMetadata } from "@yaakapp-internal/proxy-lib";
|
||||
import { rpc } from "./rpc";
|
||||
|
||||
type ActionBinding = {
|
||||
invocation: ActionInvocation;
|
||||
meta: ActionMetadata;
|
||||
keys: { key: string; ctrl: boolean; shift: boolean; alt: boolean; meta: boolean };
|
||||
};
|
||||
|
||||
/** Parse a hotkey string like "Ctrl+Shift+P" into its parts. */
|
||||
function parseHotkey(hotkey: string): ActionBinding["keys"] {
|
||||
const parts = hotkey.split("+").map((p) => p.trim().toLowerCase());
|
||||
return {
|
||||
ctrl: parts.includes("ctrl") || parts.includes("control"),
|
||||
shift: parts.includes("shift"),
|
||||
alt: parts.includes("alt"),
|
||||
meta: parts.includes("meta") || parts.includes("cmd") || parts.includes("command"),
|
||||
key:
|
||||
parts.filter(
|
||||
(p) => !["ctrl", "control", "shift", "alt", "meta", "cmd", "command"].includes(p),
|
||||
)[0] ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
function matchesEvent(binding: ActionBinding["keys"], e: KeyboardEvent): boolean {
|
||||
return (
|
||||
e.ctrlKey === binding.ctrl &&
|
||||
e.shiftKey === binding.shift &&
|
||||
e.altKey === binding.alt &&
|
||||
e.metaKey === binding.meta &&
|
||||
e.key.toLowerCase() === binding.key
|
||||
);
|
||||
}
|
||||
|
||||
/** Fetch all actions from Rust and register a global keydown listener. */
|
||||
export async function initHotkeys(): Promise<() => void> {
|
||||
const { actions } = await rpc("list_actions", {});
|
||||
|
||||
const bindings: ActionBinding[] = actions
|
||||
.filter(
|
||||
// oxlint-disable-next-line no-redundant-type-constituents -- ActionMetadata resolves at runtime
|
||||
(entry): entry is [ActionInvocation, ActionMetadata & { defaultHotkey: string }] =>
|
||||
entry[1].defaultHotkey != null,
|
||||
)
|
||||
.map(([invocation, meta]) => ({
|
||||
invocation,
|
||||
meta,
|
||||
keys: parseHotkey(meta.defaultHotkey),
|
||||
}));
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
for (const binding of bindings) {
|
||||
if (matchesEvent(binding.keys, e)) {
|
||||
e.preventDefault();
|
||||
void rpc("execute_action", binding.invocation);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => window.removeEventListener("keydown", onKeyDown);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { RpcEventSchema, RpcSchema } from "@yaakapp-internal/proxy-lib";
|
||||
import { command, subscribe } from "./tauri";
|
||||
|
||||
export type Req<K extends keyof RpcSchema> = RpcSchema[K][0];
|
||||
export type Res<K extends keyof RpcSchema> = RpcSchema[K][1];
|
||||
|
||||
export async function rpc<K extends keyof RpcSchema>(cmd: K, payload: Req<K>): Promise<Res<K>> {
|
||||
return command<Res<K>>("rpc", { cmd, payload });
|
||||
}
|
||||
|
||||
/** Subscribe to a backend event. Returns an unsubscribe function. */
|
||||
export function listen<K extends keyof RpcEventSchema>(
|
||||
event: K & string,
|
||||
callback: (payload: RpcEventSchema[K]) => void,
|
||||
): () => void {
|
||||
return subscribe<RpcEventSchema[K]>(event, callback);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { createModelStore } from "@yaakapp-internal/model-store";
|
||||
import type { HttpExchange } from "@yaakapp-internal/proxy-lib";
|
||||
|
||||
type ProxyModels = {
|
||||
http_exchange: HttpExchange;
|
||||
};
|
||||
|
||||
export const { dataAtom, applyChange, replaceAll, listAtom, orderedListAtom } =
|
||||
createModelStore<ProxyModels>(["http_exchange"]);
|
||||
|
||||
export const httpExchangesAtom = orderedListAtom("http_exchange", "createdAt", "desc");
|
||||
@@ -1,30 +0,0 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen as tauriListen } from "@tauri-apps/api/event";
|
||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import { type as tauriOsType } from "@tauri-apps/plugin-os";
|
||||
|
||||
/** Call a Tauri command. */
|
||||
export function command<T>(cmd: string, args?: Record<string, unknown>): Promise<T> {
|
||||
return invoke(cmd, args) as Promise<T>;
|
||||
}
|
||||
|
||||
/** Subscribe to a Tauri event. Returns an unsubscribe function. */
|
||||
export function subscribe<T>(event: string, callback: (payload: T) => void): () => void {
|
||||
let unsub: (() => void) | null = null;
|
||||
tauriListen<T>(event, (e) => callback(e.payload))
|
||||
.then((fn) => {
|
||||
unsub = fn;
|
||||
})
|
||||
.catch(console.error);
|
||||
return () => unsub?.();
|
||||
}
|
||||
|
||||
/** Show the current webview window. */
|
||||
export function showWindow(): Promise<void> {
|
||||
return getCurrentWebviewWindow().show();
|
||||
}
|
||||
|
||||
/** Get the current OS type (e.g. "macos", "linux", "windows"). */
|
||||
export function getOsType() {
|
||||
return tauriOsType();
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { setWindowTheme } from "@yaakapp-internal/mac-window";
|
||||
import {
|
||||
applyThemeToDocument,
|
||||
defaultDarkTheme,
|
||||
defaultLightTheme,
|
||||
getCSSAppearance,
|
||||
platformFromUserAgent,
|
||||
setPlatformOnDocument,
|
||||
subscribeToPreferredAppearance,
|
||||
type Appearance,
|
||||
} from "@yaakapp-internal/theme";
|
||||
import { showWindow } from "./tauri";
|
||||
|
||||
setPlatformOnDocument(platformFromUserAgent(navigator.userAgent));
|
||||
|
||||
// Apply a quick initial theme based on CSS media query
|
||||
let preferredAppearance: Appearance = getCSSAppearance();
|
||||
applyTheme(preferredAppearance);
|
||||
|
||||
// Then subscribe to accurate OS appearance detection and changes
|
||||
subscribeToPreferredAppearance((a) => {
|
||||
preferredAppearance = a;
|
||||
applyTheme(preferredAppearance);
|
||||
});
|
||||
|
||||
// Show window after initial theme is applied (window starts hidden to prevent flash)
|
||||
showWindow().catch(console.error);
|
||||
|
||||
function applyTheme(appearance: Appearance) {
|
||||
const theme = appearance === "dark" ? defaultDarkTheme : defaultLightTheme;
|
||||
applyThemeToDocument(theme);
|
||||
if (theme.base.surface != null) {
|
||||
setWindowTheme(theme.base.surface);
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
@apply w-full h-full overflow-hidden text-text bg-surface;
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-family-interface: "";
|
||||
--font-family-editor: "";
|
||||
}
|
||||
|
||||
:root {
|
||||
font-variant-ligatures: none;
|
||||
}
|
||||
|
||||
html[data-platform="linux"] {
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
::selection {
|
||||
@apply bg-selection;
|
||||
}
|
||||
|
||||
:not(a),
|
||||
:not(input):not(textarea),
|
||||
:not(input):not(textarea)::after,
|
||||
:not(input):not(textarea)::before {
|
||||
@apply select-none cursor-default;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
&::placeholder {
|
||||
@apply text-placeholder;
|
||||
}
|
||||
}
|
||||
|
||||
a,
|
||||
a[href] * {
|
||||
@apply cursor-pointer !important;
|
||||
}
|
||||
|
||||
table th {
|
||||
@apply text-left;
|
||||
}
|
||||
|
||||
:not(iframe) {
|
||||
&::-webkit-scrollbar,
|
||||
&::-webkit-scrollbar-corner {
|
||||
@apply w-[8px] h-[8px] bg-transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
@apply bg-text-subtlest rounded-[4px] opacity-20;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
@apply opacity-40 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.hide-scrollbars {
|
||||
&::-webkit-scrollbar-corner,
|
||||
&::-webkit-scrollbar {
|
||||
@apply hidden !important;
|
||||
}
|
||||
}
|
||||
|
||||
.rtl {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--transition-duration: 100ms ease-in-out;
|
||||
--color-white: 255 100% 100%;
|
||||
--color-black: 255 0% 0%;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { createStore, Provider } from "jotai";
|
||||
import { LazyMotion, MotionConfig } from "motion/react";
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { ProxyLayout } from "./components/ProxyLayout";
|
||||
import { listen, rpc } from "./lib/rpc";
|
||||
import { initHotkeys } from "./lib/hotkeys";
|
||||
import { applyChange, dataAtom, replaceAll } from "./lib/store";
|
||||
import "./main.css";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
const jotaiStore = createStore();
|
||||
|
||||
// Load initial models from the database
|
||||
void rpc("list_models", {}).then((res) => {
|
||||
jotaiStore.set(dataAtom, (prev) => replaceAll(prev, "http_exchange", res.httpExchanges));
|
||||
});
|
||||
|
||||
// Register hotkeys from action metadata
|
||||
void initHotkeys();
|
||||
|
||||
// Subscribe to model change events from the backend
|
||||
void listen("model_write", (payload) => {
|
||||
jotaiStore.set(dataAtom, (prev) =>
|
||||
applyChange(prev, "http_exchange", payload.model, payload.change),
|
||||
);
|
||||
});
|
||||
|
||||
const motionFeatures = () => import("framer-motion").then((mod) => mod.domAnimation);
|
||||
|
||||
createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={jotaiStore}>
|
||||
<LazyMotion strict features={motionFeatures}>
|
||||
<MotionConfig transition={{ duration: 0.1 }}>
|
||||
<ProxyLayout />
|
||||
</MotionConfig>
|
||||
</LazyMotion>
|
||||
</Provider>
|
||||
</QueryClientProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"name": "@yaakapp/yaak-proxy",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev --force",
|
||||
"build": "vite build",
|
||||
"lint": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.90.5",
|
||||
"@tauri-apps/api": "^2.9.1",
|
||||
"@tauri-apps/plugin-os": "^2.3.2",
|
||||
"@yaakapp-internal/model-store": "^1.0.0",
|
||||
"@yaakapp-internal/proxy-lib": "^1.0.0",
|
||||
"@yaakapp-internal/theme": "^1.0.0",
|
||||
"@yaakapp-internal/ui": "^1.0.0",
|
||||
"classnames": "^2.5.1",
|
||||
"jotai": "^2.18.0",
|
||||
"motion": "^12.4.7",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^7.0.8"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require("@tailwindcss/nesting")(require("postcss-nesting")),
|
||||
require("tailwindcss"),
|
||||
require("autoprefixer"),
|
||||
],
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
const sharedConfig = require("@yaakapp-internal/tailwind-config");
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
...sharedConfig,
|
||||
content: ["./**/*.{html,ts,tsx}", "../../packages/ui/src/**/*.{ts,tsx}"],
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2021",
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"useDefineForClassFields": true,
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@yaakapp-internal/theme": ["../../packages/theme/src/index.ts"],
|
||||
"@yaakapp-internal/theme/*": ["../../packages/theme/src/*"],
|
||||
"@yaakapp-internal/ui": ["../../packages/ui/src/index.ts"],
|
||||
"@yaakapp-internal/ui/*": ["../../packages/ui/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["."],
|
||||
"exclude": ["vite.config.ts"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noUncheckedIndexedAccess": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
1
apps/yaak-proxy/vite-env.d.ts
vendored
@@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -1,16 +0,0 @@
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: "../../dist/apps/yaak-proxy",
|
||||
emptyOutDir: true,
|
||||
},
|
||||
clearScreen: false,
|
||||
server: {
|
||||
port: parseInt(process.env.YAAK_PROXY_DEV_PORT ?? "2420", 10),
|
||||
strictPort: true,
|
||||
},
|
||||
envPrefix: ["VITE_", "TAURI_"],
|
||||
});
|
||||
80
biome.json
@@ -1,80 +0,0 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.3.13/schema.json",
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"a11y": {
|
||||
"useKeyWithClickEvents": "off"
|
||||
},
|
||||
"style": {
|
||||
"noRestrictedImports": {
|
||||
"level": "error",
|
||||
"options": {
|
||||
"paths": {
|
||||
"@tauri-apps/api/core": "Use lib/tauri.ts instead of importing @tauri-apps directly",
|
||||
"@tauri-apps/api/event": "Use lib/tauri.ts instead of importing @tauri-apps directly",
|
||||
"@tauri-apps/api/webviewWindow": "Use lib/tauri.ts instead of importing @tauri-apps directly",
|
||||
"@tauri-apps/plugin-os": "Use lib/tauri.ts instead of importing @tauri-apps directly"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 100,
|
||||
"bracketSpacing": true
|
||||
},
|
||||
"css": {
|
||||
"parser": {
|
||||
"tailwindDirectives": true
|
||||
},
|
||||
"linter": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "single",
|
||||
"jsxQuoteStyle": "double",
|
||||
"trailingCommas": "all",
|
||||
"semicolons": "always"
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"includes": ["apps/yaak-proxy/lib/tauri.ts"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
"noRestrictedImports": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"files": {
|
||||
"includes": [
|
||||
"**",
|
||||
"!**/node_modules",
|
||||
"!**/dist",
|
||||
"!**/build",
|
||||
"!target",
|
||||
"!scripts",
|
||||
"!crates",
|
||||
"!crates-tauri",
|
||||
"!apps/yaak-client/tailwind.config.cjs",
|
||||
"!apps/yaak-client/postcss.config.cjs",
|
||||
"!apps/yaak-client/vite.config.ts",
|
||||
"!apps/yaak-client/routeTree.gen.ts",
|
||||
"!packages/plugin-runtime-types/lib",
|
||||
"!**/bindings",
|
||||
"!flatpak",
|
||||
"!npm"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -6,17 +6,17 @@ use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use yaak_crypto::manager::EncryptionManager;
|
||||
use yaak_models::blob_manager::BlobManager;
|
||||
use yaak_models::client_db::ClientDb;
|
||||
use yaak_models::db_context::DbContext;
|
||||
use yaak_models::query_manager::QueryManager;
|
||||
use yaak_plugins::events::PluginContext;
|
||||
use yaak_plugins::manager::PluginManager;
|
||||
|
||||
const EMBEDDED_PLUGIN_RUNTIME: &str = include_str!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../crates-tauri/yaak-app-client/vendored/plugin-runtime/index.cjs"
|
||||
"/../../crates-tauri/yaak-app/vendored/plugin-runtime/index.cjs"
|
||||
));
|
||||
static EMBEDDED_VENDORED_PLUGINS: Dir<'_> =
|
||||
include_dir!("$CARGO_MANIFEST_DIR/../../crates-tauri/yaak-app-client/vendored/plugins");
|
||||
include_dir!("$CARGO_MANIFEST_DIR/../../crates-tauri/yaak-app/vendored/plugins");
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct CliExecutionContext {
|
||||
@@ -108,7 +108,7 @@ impl CliContext {
|
||||
&self.data_dir
|
||||
}
|
||||
|
||||
pub fn db(&self) -> ClientDb<'_> {
|
||||
pub fn db(&self) -> DbContext<'_> {
|
||||
self.query_manager.connect()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
[package]
|
||||
name = "yaak-proxy-lib"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
authors = ["Gregory Schier"]
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
log = { workspace = true }
|
||||
include_dir = "0.7"
|
||||
r2d2 = "0.8.10"
|
||||
r2d2_sqlite = "0.25.0"
|
||||
rusqlite = { version = "0.32.1", features = ["bundled", "chrono"] }
|
||||
sea-query = { version = "0.32.1", features = ["with-chrono", "attr"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
ts-rs = { workspace = true, features = ["chrono-impl"] }
|
||||
yaak-database = { workspace = true }
|
||||
yaak-proxy = { workspace = true }
|
||||
yaak-rpc = { workspace = true }
|
||||
@@ -1,3 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ModelChangeEvent = { "type": "upsert", created: boolean, } | { "type": "delete" };
|
||||
@@ -1,8 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ModelChangeEvent } from "./ModelChangeEvent";
|
||||
|
||||
export type HttpExchange = { id: string, createdAt: string, updatedAt: string, url: string, method: string, reqHeaders: Array<ProxyHeader>, reqBody: Array<number> | null, resStatus: number | null, resHeaders: Array<ProxyHeader>, resBody: Array<number> | null, error: string | null, };
|
||||
|
||||
export type ModelPayload = { model: HttpExchange, change: ModelChangeEvent, };
|
||||
|
||||
export type ProxyHeader = { name: string, value: string, };
|
||||
28
crates-proxy/yaak-proxy-lib/bindings/gen_rpc.ts
generated
@@ -1,28 +0,0 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { HttpExchange, ModelPayload } from "./gen_models";
|
||||
|
||||
export type ActionInvocation = { "scope": "global", action: GlobalAction, };
|
||||
|
||||
export type ActionMetadata = { label: string, defaultHotkey: string | null, };
|
||||
|
||||
export type GetProxyStateRequest = Record<string, never>;
|
||||
|
||||
export type GetProxyStateResponse = { state: ProxyState, };
|
||||
|
||||
export type GlobalAction = "proxy_start" | "proxy_stop";
|
||||
|
||||
export type ListActionsRequest = Record<string, never>;
|
||||
|
||||
export type ListActionsResponse = { actions: Array<[ActionInvocation, ActionMetadata]>, };
|
||||
|
||||
export type ListModelsRequest = Record<string, never>;
|
||||
|
||||
export type ListModelsResponse = { httpExchanges: Array<HttpExchange>, };
|
||||
|
||||
export type ProxyState = "running" | "stopped";
|
||||
|
||||
export type ProxyStatePayload = { state: ProxyState, };
|
||||
|
||||
export type RpcEventSchema = { model_write: ModelPayload, proxy_state_changed: ProxyStatePayload, };
|
||||
|
||||
export type RpcSchema = { execute_action: [ActionInvocation, boolean], get_proxy_state: [GetProxyStateRequest, GetProxyStateResponse], list_actions: [ListActionsRequest, ListActionsResponse], list_models: [ListModelsRequest, ListModelsResponse], };
|
||||
2
crates-proxy/yaak-proxy-lib/bindings/index.ts
generated
@@ -1,2 +0,0 @@
|
||||
export * from "./gen_rpc";
|
||||
export * from "./gen_models";
|
||||
@@ -1,14 +0,0 @@
|
||||
CREATE TABLE http_exchanges
|
||||
(
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
url TEXT NOT NULL DEFAULT '',
|
||||
method TEXT NOT NULL DEFAULT '',
|
||||
req_headers TEXT NOT NULL DEFAULT '[]',
|
||||
req_body BLOB,
|
||||
res_status INTEGER,
|
||||
res_headers TEXT NOT NULL DEFAULT '[]',
|
||||
res_body BLOB,
|
||||
error TEXT
|
||||
);
|
||||
@@ -1,53 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "gen_rpc.ts")]
|
||||
pub enum GlobalAction {
|
||||
ProxyStart,
|
||||
ProxyStop,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(tag = "scope", rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "gen_rpc.ts")]
|
||||
pub enum ActionInvocation {
|
||||
Global { action: GlobalAction },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_rpc.ts")]
|
||||
pub struct ActionMetadata {
|
||||
pub label: String,
|
||||
pub default_hotkey: Option<String>,
|
||||
}
|
||||
|
||||
fn default_hotkey(mac: &str, other: &str) -> Option<String> {
|
||||
if cfg!(target_os = "macos") {
|
||||
Some(mac.into())
|
||||
} else {
|
||||
Some(other.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// All global actions with their metadata, used by `list_actions` RPC.
|
||||
pub fn all_global_actions() -> Vec<(ActionInvocation, ActionMetadata)> {
|
||||
vec![
|
||||
(
|
||||
ActionInvocation::Global { action: GlobalAction::ProxyStart },
|
||||
ActionMetadata {
|
||||
label: "Start Proxy".into(),
|
||||
default_hotkey: default_hotkey("Meta+Shift+P", "Ctrl+Shift+P"),
|
||||
},
|
||||
),
|
||||
(
|
||||
ActionInvocation::Global { action: GlobalAction::ProxyStop },
|
||||
ActionMetadata {
|
||||
label: "Stop Proxy".into(),
|
||||
default_hotkey: default_hotkey("Meta+Shift+S", "Ctrl+Shift+S"),
|
||||
},
|
||||
),
|
||||
]
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
use include_dir::{Dir, include_dir};
|
||||
use r2d2::Pool;
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
use std::path::Path;
|
||||
use yaak_database::{ConnectionOrTx, DbContext, run_migrations};
|
||||
|
||||
static MIGRATIONS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/migrations");
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ProxyQueryManager {
|
||||
pool: Pool<SqliteConnectionManager>,
|
||||
}
|
||||
|
||||
impl ProxyQueryManager {
|
||||
pub fn new(db_path: &Path) -> Self {
|
||||
let manager = SqliteConnectionManager::file(db_path);
|
||||
let pool = Pool::builder()
|
||||
.max_size(5)
|
||||
.build(manager)
|
||||
.expect("Failed to create proxy DB pool");
|
||||
run_migrations(&pool, &MIGRATIONS).expect("Failed to run proxy DB migrations");
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
pub fn with_conn<F, T>(&self, func: F) -> T
|
||||
where
|
||||
F: FnOnce(&DbContext) -> T,
|
||||
{
|
||||
let conn = self.pool.get().expect("Failed to get proxy DB connection");
|
||||
let ctx = DbContext::new(ConnectionOrTx::Connection(conn));
|
||||
func(&ctx)
|
||||
}
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
pub mod actions;
|
||||
pub mod db;
|
||||
pub mod models;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::Mutex;
|
||||
use log::warn;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
use yaak_database::{ModelChangeEvent, UpdateSource};
|
||||
use yaak_proxy::{CapturedRequest, ProxyEvent, ProxyHandle, RequestState};
|
||||
use yaak_rpc::{RpcError, RpcEventEmitter, define_rpc};
|
||||
use crate::actions::{ActionInvocation, ActionMetadata, GlobalAction};
|
||||
use crate::db::ProxyQueryManager;
|
||||
use crate::models::{HttpExchange, ModelPayload, ProxyHeader};
|
||||
|
||||
// -- Context --
|
||||
|
||||
pub struct ProxyCtx {
|
||||
handle: Mutex<Option<ProxyHandle>>,
|
||||
pub db: ProxyQueryManager,
|
||||
pub events: RpcEventEmitter,
|
||||
}
|
||||
|
||||
impl ProxyCtx {
|
||||
pub fn new(db_path: &Path, events: RpcEventEmitter) -> Self {
|
||||
Self {
|
||||
handle: Mutex::new(None),
|
||||
db: ProxyQueryManager::new(db_path),
|
||||
events,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- Proxy state --
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "gen_rpc.ts")]
|
||||
pub enum ProxyState {
|
||||
Running,
|
||||
Stopped,
|
||||
}
|
||||
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export, export_to = "gen_rpc.ts")]
|
||||
pub struct ProxyStatePayload {
|
||||
pub state: ProxyState,
|
||||
}
|
||||
|
||||
// -- Request/response types --
|
||||
|
||||
#[derive(Deserialize, TS)]
|
||||
#[ts(export, export_to = "gen_rpc.ts")]
|
||||
pub struct ListActionsRequest {}
|
||||
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export, export_to = "gen_rpc.ts")]
|
||||
pub struct ListActionsResponse {
|
||||
pub actions: Vec<(ActionInvocation, ActionMetadata)>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, TS)]
|
||||
#[ts(export, export_to = "gen_rpc.ts")]
|
||||
pub struct ListModelsRequest {}
|
||||
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export, export_to = "gen_rpc.ts")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ListModelsResponse {
|
||||
pub http_exchanges: Vec<HttpExchange>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, TS)]
|
||||
#[ts(export, export_to = "gen_rpc.ts")]
|
||||
pub struct GetProxyStateRequest {}
|
||||
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export, export_to = "gen_rpc.ts")]
|
||||
pub struct GetProxyStateResponse {
|
||||
pub state: ProxyState,
|
||||
}
|
||||
|
||||
// -- Handlers --
|
||||
|
||||
fn execute_action(ctx: &ProxyCtx, invocation: ActionInvocation) -> Result<bool, RpcError> {
|
||||
match invocation {
|
||||
ActionInvocation::Global { action } => match action {
|
||||
GlobalAction::ProxyStart => {
|
||||
let mut handle = ctx
|
||||
.handle
|
||||
.lock()
|
||||
.map_err(|_| RpcError { message: "lock poisoned".into() })?;
|
||||
|
||||
if handle.is_some() {
|
||||
return Ok(true); // already running
|
||||
}
|
||||
|
||||
let mut proxy_handle = yaak_proxy::start_proxy(9090)
|
||||
.map_err(|e| RpcError { message: e })?;
|
||||
|
||||
if let Some(event_rx) = proxy_handle.take_event_rx() {
|
||||
let db = ctx.db.clone();
|
||||
let events = ctx.events.clone();
|
||||
std::thread::spawn(move || run_event_loop(event_rx, db, events));
|
||||
}
|
||||
|
||||
*handle = Some(proxy_handle);
|
||||
ctx.events.emit("proxy_state_changed", &ProxyStatePayload {
|
||||
state: ProxyState::Running,
|
||||
});
|
||||
Ok(true)
|
||||
}
|
||||
GlobalAction::ProxyStop => {
|
||||
let mut handle = ctx
|
||||
.handle
|
||||
.lock()
|
||||
.map_err(|_| RpcError { message: "lock poisoned".into() })?;
|
||||
handle.take();
|
||||
ctx.events.emit("proxy_state_changed", &ProxyStatePayload {
|
||||
state: ProxyState::Stopped,
|
||||
});
|
||||
Ok(true)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn get_proxy_state(ctx: &ProxyCtx, _req: GetProxyStateRequest) -> Result<GetProxyStateResponse, RpcError> {
|
||||
let handle = ctx
|
||||
.handle
|
||||
.lock()
|
||||
.map_err(|_| RpcError { message: "lock poisoned".into() })?;
|
||||
let state = if handle.is_some() {
|
||||
ProxyState::Running
|
||||
} else {
|
||||
ProxyState::Stopped
|
||||
};
|
||||
Ok(GetProxyStateResponse { state })
|
||||
}
|
||||
|
||||
fn list_actions(_ctx: &ProxyCtx, _req: ListActionsRequest) -> Result<ListActionsResponse, RpcError> {
|
||||
Ok(ListActionsResponse {
|
||||
actions: crate::actions::all_global_actions(),
|
||||
})
|
||||
}
|
||||
|
||||
fn list_models(ctx: &ProxyCtx, _req: ListModelsRequest) -> Result<ListModelsResponse, RpcError> {
|
||||
ctx.db.with_conn(|db| {
|
||||
Ok(ListModelsResponse {
|
||||
http_exchanges: db.find_all::<HttpExchange>()
|
||||
.map_err(|e| RpcError { message: e.to_string() })?,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// -- Event loop --
|
||||
|
||||
fn run_event_loop(rx: std::sync::mpsc::Receiver<ProxyEvent>, db: ProxyQueryManager, events: RpcEventEmitter) {
|
||||
let mut in_flight: HashMap<u64, CapturedRequest> = HashMap::new();
|
||||
|
||||
while let Ok(event) = rx.recv() {
|
||||
match event {
|
||||
ProxyEvent::RequestStart { id, method, url, http_version } => {
|
||||
in_flight.insert(id, CapturedRequest {
|
||||
id,
|
||||
method,
|
||||
url,
|
||||
http_version,
|
||||
status: None,
|
||||
elapsed_ms: None,
|
||||
remote_http_version: None,
|
||||
request_headers: vec![],
|
||||
request_body: None,
|
||||
response_headers: vec![],
|
||||
response_body: None,
|
||||
response_body_size: 0,
|
||||
state: RequestState::Sending,
|
||||
error: None,
|
||||
});
|
||||
}
|
||||
ProxyEvent::RequestHeader { id, name, value } => {
|
||||
if let Some(r) = in_flight.get_mut(&id) {
|
||||
r.request_headers.push((name, value));
|
||||
}
|
||||
}
|
||||
ProxyEvent::RequestBody { id, body } => {
|
||||
if let Some(r) = in_flight.get_mut(&id) {
|
||||
r.request_body = Some(body);
|
||||
}
|
||||
}
|
||||
ProxyEvent::ResponseStart { id, status, http_version, elapsed_ms } => {
|
||||
if let Some(r) = in_flight.get_mut(&id) {
|
||||
r.status = Some(status);
|
||||
r.remote_http_version = Some(http_version);
|
||||
r.elapsed_ms = Some(elapsed_ms);
|
||||
r.state = RequestState::Receiving;
|
||||
}
|
||||
}
|
||||
ProxyEvent::ResponseHeader { id, name, value } => {
|
||||
if let Some(r) = in_flight.get_mut(&id) {
|
||||
r.response_headers.push((name, value));
|
||||
}
|
||||
}
|
||||
ProxyEvent::ResponseBodyChunk { .. } => {
|
||||
// Progress only — no action needed
|
||||
}
|
||||
ProxyEvent::ResponseBodyComplete { id, body, size, elapsed_ms } => {
|
||||
if let Some(mut r) = in_flight.remove(&id) {
|
||||
r.response_body = body;
|
||||
r.response_body_size = size;
|
||||
r.elapsed_ms = r.elapsed_ms.or(Some(elapsed_ms));
|
||||
r.state = RequestState::Complete;
|
||||
write_entry(&db, &events, &r);
|
||||
}
|
||||
}
|
||||
ProxyEvent::Error { id, error } => {
|
||||
if let Some(mut r) = in_flight.remove(&id) {
|
||||
r.error = Some(error);
|
||||
r.state = RequestState::Error;
|
||||
write_entry(&db, &events, &r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_entry(db: &ProxyQueryManager, events: &RpcEventEmitter, r: &CapturedRequest) {
|
||||
let entry = HttpExchange {
|
||||
url: r.url.clone(),
|
||||
method: r.method.clone(),
|
||||
req_headers: r.request_headers.iter()
|
||||
.map(|(n, v)| ProxyHeader { name: n.clone(), value: v.clone() })
|
||||
.collect(),
|
||||
req_body: r.request_body.clone(),
|
||||
res_status: r.status.map(|s| s as i32),
|
||||
res_headers: r.response_headers.iter()
|
||||
.map(|(n, v)| ProxyHeader { name: n.clone(), value: v.clone() })
|
||||
.collect(),
|
||||
res_body: r.response_body.clone(),
|
||||
error: r.error.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
db.with_conn(|ctx| {
|
||||
match ctx.upsert(&entry, &UpdateSource::Background) {
|
||||
Ok((saved, created)) => {
|
||||
events.emit("model_write", &ModelPayload {
|
||||
model: saved,
|
||||
change: ModelChangeEvent::Upsert { created },
|
||||
});
|
||||
}
|
||||
Err(e) => warn!("Failed to write proxy entry: {e}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// -- Router + Schema --
|
||||
|
||||
define_rpc! {
|
||||
ProxyCtx;
|
||||
commands {
|
||||
execute_action(ActionInvocation) -> bool,
|
||||
get_proxy_state(GetProxyStateRequest) -> GetProxyStateResponse,
|
||||
list_actions(ListActionsRequest) -> ListActionsResponse,
|
||||
list_models(ListModelsRequest) -> ListModelsResponse,
|
||||
}
|
||||
events {
|
||||
model_write(ModelPayload),
|
||||
proxy_state_changed(ProxyStatePayload),
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use rusqlite::Row;
|
||||
use sea_query::{IntoColumnRef, IntoIden, IntoTableRef, Order, SimpleExpr, enum_def};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
use yaak_database::{ModelChangeEvent, Result as DbResult, UpdateSource, UpsertModelInfo, generate_prefixed_id, upsert_date};
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
pub struct ProxyHeader {
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
#[enum_def(table_name = "http_exchanges")]
|
||||
pub struct HttpExchange {
|
||||
pub id: String,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub updated_at: NaiveDateTime,
|
||||
pub url: String,
|
||||
pub method: String,
|
||||
pub req_headers: Vec<ProxyHeader>,
|
||||
pub req_body: Option<Vec<u8>>,
|
||||
pub res_status: Option<i32>,
|
||||
pub res_headers: Vec<ProxyHeader>,
|
||||
pub res_body: Option<Vec<u8>>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
pub struct ModelPayload {
|
||||
pub model: HttpExchange,
|
||||
pub change: ModelChangeEvent,
|
||||
}
|
||||
|
||||
impl UpsertModelInfo for HttpExchange {
|
||||
fn table_name() -> impl IntoTableRef + IntoIden {
|
||||
HttpExchangeIden::Table
|
||||
}
|
||||
|
||||
fn id_column() -> impl IntoIden + Eq + Clone {
|
||||
HttpExchangeIden::Id
|
||||
}
|
||||
|
||||
fn generate_id() -> String {
|
||||
generate_prefixed_id("he")
|
||||
}
|
||||
|
||||
fn order_by() -> (impl IntoColumnRef, Order) {
|
||||
(HttpExchangeIden::CreatedAt, Order::Desc)
|
||||
}
|
||||
|
||||
fn get_id(&self) -> String {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
fn insert_values(
|
||||
self,
|
||||
source: &UpdateSource,
|
||||
) -> DbResult<Vec<(impl IntoIden + Eq, impl Into<SimpleExpr>)>> {
|
||||
use HttpExchangeIden::*;
|
||||
Ok(vec![
|
||||
(CreatedAt, upsert_date(source, self.created_at)),
|
||||
(UpdatedAt, upsert_date(source, self.updated_at)),
|
||||
(Url, self.url.into()),
|
||||
(Method, self.method.into()),
|
||||
(ReqHeaders, serde_json::to_string(&self.req_headers)?.into()),
|
||||
(ReqBody, self.req_body.into()),
|
||||
(ResStatus, self.res_status.into()),
|
||||
(ResHeaders, serde_json::to_string(&self.res_headers)?.into()),
|
||||
(ResBody, self.res_body.into()),
|
||||
(Error, self.error.into()),
|
||||
])
|
||||
}
|
||||
|
||||
fn update_columns() -> Vec<impl IntoIden> {
|
||||
vec![
|
||||
HttpExchangeIden::UpdatedAt,
|
||||
HttpExchangeIden::Url,
|
||||
HttpExchangeIden::Method,
|
||||
HttpExchangeIden::ReqHeaders,
|
||||
HttpExchangeIden::ReqBody,
|
||||
HttpExchangeIden::ResStatus,
|
||||
HttpExchangeIden::ResHeaders,
|
||||
HttpExchangeIden::ResBody,
|
||||
HttpExchangeIden::Error,
|
||||
]
|
||||
}
|
||||
|
||||
fn from_row(r: &Row) -> rusqlite::Result<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let req_headers: String = r.get("req_headers")?;
|
||||
let res_headers: String = r.get("res_headers")?;
|
||||
Ok(Self {
|
||||
id: r.get("id")?,
|
||||
created_at: r.get("created_at")?,
|
||||
updated_at: r.get("updated_at")?,
|
||||
url: r.get("url")?,
|
||||
method: r.get("method")?,
|
||||
req_headers: serde_json::from_str(&req_headers).unwrap_or_default(),
|
||||
req_body: r.get("req_body")?,
|
||||
res_status: r.get("res_status")?,
|
||||
res_headers: serde_json::from_str(&res_headers).unwrap_or_default(),
|
||||
res_body: r.get("res_body")?,
|
||||
error: r.get("error")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "@yaakapp-internal/tauri-client",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "bindings/index.ts"
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
tauri_app_client_lib::run();
|
||||
}
|
||||
8
crates-tauri/yaak-app-proxy/.gitignore
vendored
@@ -1,8 +0,0 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
target/
|
||||
|
||||
gen/*
|
||||
|
||||
**/permissions/autogenerated
|
||||
**/permissions/schemas
|
||||
@@ -1,23 +0,0 @@
|
||||
[package]
|
||||
name = "yaak-app-proxy"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
authors = ["Gregory Schier"]
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "tauri_app_proxy_lib"
|
||||
crate-type = ["staticlib", "cdylib", "lib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.5.3", features = [] }
|
||||
|
||||
[dependencies]
|
||||
log = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tauri = { workspace = true }
|
||||
tauri-plugin-os = "2.3.2"
|
||||
yaak-mac-window = { workspace = true }
|
||||
yaak-proxy-lib = { workspace = true }
|
||||
yaak-rpc = { workspace = true }
|
||||
yaak-window = { workspace = true }
|
||||
1
crates-tauri/yaak-app-proxy/bindings/index.ts
generated
@@ -1 +0,0 @@
|
||||
export {};
|
||||
@@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"identifier": "default",
|
||||
"description": "Default capabilities for the Yaak Proxy app",
|
||||
"windows": ["*"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"os:allow-os-type",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-is-fullscreen",
|
||||
"core:window:allow-is-maximized",
|
||||
"core:window:allow-maximize",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-unmaximize",
|
||||
"yaak-mac-window:default"
|
||||
]
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "@yaakapp-internal/tauri-proxy",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "bindings/index.ts"
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
use log::{error, info, warn};
|
||||
use tauri::{Emitter, Manager, RunEvent, State, WebviewWindow};
|
||||
use tauri::Runtime;
|
||||
use yaak_proxy_lib::ProxyCtx;
|
||||
use yaak_rpc::{RpcEventEmitter, RpcRouter};
|
||||
use yaak_window::window::CreateWindowConfig;
|
||||
|
||||
mod window_menu;
|
||||
|
||||
fn setup_window_menu<R: Runtime>(win: &WebviewWindow<R>) {
|
||||
#[allow(unused_variables)]
|
||||
let menu = match window_menu::app_menu(win.app_handle()) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
warn!("Failed to create menu: {e:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// This causes the window to not be clickable (in AppImage), so disable on Linux
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
win.app_handle().set_menu(menu).expect("Failed to set app menu");
|
||||
|
||||
let webview_window = win.clone();
|
||||
win.on_menu_event(move |w, event| {
|
||||
if !w.is_focused().unwrap() {
|
||||
return;
|
||||
}
|
||||
|
||||
let event_id = event.id().0.as_str();
|
||||
match event_id {
|
||||
"hacked_quit" => {
|
||||
w.webview_windows().iter().for_each(|(_, w)| {
|
||||
info!("Closing window {}", w.label());
|
||||
let _ = w.close();
|
||||
});
|
||||
}
|
||||
"dev.refresh" => webview_window.eval("location.reload()").unwrap(),
|
||||
"dev.toggle_devtools" => {
|
||||
if webview_window.is_devtools_open() {
|
||||
webview_window.close_devtools();
|
||||
} else {
|
||||
webview_window.open_devtools();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn rpc(
|
||||
router: State<'_, RpcRouter<ProxyCtx>>,
|
||||
ctx: State<'_, ProxyCtx>,
|
||||
cmd: String,
|
||||
payload: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
router.dispatch(&cmd, payload, &ctx).map_err(|e| e.message)
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(yaak_mac_window::init())
|
||||
.setup(|app| {
|
||||
let data_dir = app.path().app_data_dir().expect("no app data dir");
|
||||
std::fs::create_dir_all(&data_dir).expect("failed to create app data dir");
|
||||
|
||||
let (emitter, event_rx) = RpcEventEmitter::new();
|
||||
app.manage(ProxyCtx::new(&data_dir.join("proxy.db"), emitter));
|
||||
app.manage(yaak_proxy_lib::build_router());
|
||||
|
||||
// Drain RPC events and forward as Tauri events
|
||||
let app_handle = app.handle().clone();
|
||||
std::thread::spawn(move || {
|
||||
for event in event_rx {
|
||||
if let Err(e) = app_handle.emit(event.event, event.payload) {
|
||||
error!("Failed to emit RPC event: {e}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![rpc])
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while building yaak proxy tauri application")
|
||||
.run(|app_handle, event| {
|
||||
if let RunEvent::Ready = event {
|
||||
let config = CreateWindowConfig {
|
||||
url: "/",
|
||||
label: "main_0",
|
||||
title: "Yaak Proxy",
|
||||
inner_size: Some((1000.0, 700.0)),
|
||||
visible: false,
|
||||
hide_titlebar: true,
|
||||
..Default::default()
|
||||
};
|
||||
match yaak_window::window::create_window(app_handle, config) {
|
||||
Ok(win) => setup_window_menu(&win),
|
||||
Err(e) => error!("Failed to create proxy window: {e:?}"),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
pub use tauri::AppHandle;
|
||||
use tauri::Runtime;
|
||||
use tauri::menu::{
|
||||
AboutMetadata, HELP_SUBMENU_ID, Menu, MenuItemBuilder, PredefinedMenuItem, Submenu,
|
||||
WINDOW_SUBMENU_ID,
|
||||
};
|
||||
|
||||
pub fn app_menu<R: Runtime>(app_handle: &AppHandle<R>) -> tauri::Result<Menu<R>> {
|
||||
let pkg_info = app_handle.package_info();
|
||||
let config = app_handle.config();
|
||||
let about_metadata = AboutMetadata {
|
||||
name: Some(pkg_info.name.clone()),
|
||||
version: Some(pkg_info.version.to_string()),
|
||||
copyright: config.bundle.copyright.clone(),
|
||||
authors: config.bundle.publisher.clone().map(|p| vec![p]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let window_menu = Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
WINDOW_SUBMENU_ID,
|
||||
"Window",
|
||||
true,
|
||||
&[
|
||||
&PredefinedMenuItem::minimize(app_handle, None)?,
|
||||
&PredefinedMenuItem::maximize(app_handle, None)?,
|
||||
#[cfg(target_os = "macos")]
|
||||
&PredefinedMenuItem::separator(app_handle)?,
|
||||
&PredefinedMenuItem::close_window(app_handle, None)?,
|
||||
],
|
||||
)?;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
window_menu.set_as_windows_menu_for_nsapp()?;
|
||||
}
|
||||
|
||||
let help_menu = Submenu::with_id_and_items(
|
||||
app_handle,
|
||||
HELP_SUBMENU_ID,
|
||||
"Help",
|
||||
true,
|
||||
&[
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
&PredefinedMenuItem::about(app_handle, None, Some(about_metadata.clone()))?,
|
||||
],
|
||||
)?;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
help_menu.set_as_windows_menu_for_nsapp()?;
|
||||
}
|
||||
|
||||
let menu = Menu::with_items(
|
||||
app_handle,
|
||||
&[
|
||||
#[cfg(target_os = "macos")]
|
||||
&Submenu::with_items(
|
||||
app_handle,
|
||||
pkg_info.name.clone(),
|
||||
true,
|
||||
&[
|
||||
&PredefinedMenuItem::about(app_handle, None, Some(about_metadata))?,
|
||||
&PredefinedMenuItem::separator(app_handle)?,
|
||||
&PredefinedMenuItem::services(app_handle, None)?,
|
||||
&PredefinedMenuItem::separator(app_handle)?,
|
||||
&PredefinedMenuItem::hide(app_handle, None)?,
|
||||
&PredefinedMenuItem::hide_others(app_handle, None)?,
|
||||
&PredefinedMenuItem::separator(app_handle)?,
|
||||
&MenuItemBuilder::with_id(
|
||||
"hacked_quit".to_string(),
|
||||
format!("Quit {}", app_handle.package_info().name),
|
||||
)
|
||||
.accelerator("CmdOrCtrl+q")
|
||||
.build(app_handle)?,
|
||||
],
|
||||
)?,
|
||||
#[cfg(not(any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
)))]
|
||||
&Submenu::with_items(
|
||||
app_handle,
|
||||
"File",
|
||||
true,
|
||||
&[
|
||||
&PredefinedMenuItem::close_window(app_handle, None)?,
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
&PredefinedMenuItem::quit(app_handle, None)?,
|
||||
],
|
||||
)?,
|
||||
&Submenu::with_items(
|
||||
app_handle,
|
||||
"Edit",
|
||||
true,
|
||||
&[
|
||||
&PredefinedMenuItem::undo(app_handle, None)?,
|
||||
&PredefinedMenuItem::redo(app_handle, None)?,
|
||||
&PredefinedMenuItem::separator(app_handle)?,
|
||||
&PredefinedMenuItem::cut(app_handle, None)?,
|
||||
&PredefinedMenuItem::copy(app_handle, None)?,
|
||||
&PredefinedMenuItem::paste(app_handle, None)?,
|
||||
&PredefinedMenuItem::select_all(app_handle, None)?,
|
||||
],
|
||||
)?,
|
||||
&Submenu::with_items(
|
||||
app_handle,
|
||||
"View",
|
||||
true,
|
||||
&[
|
||||
#[cfg(target_os = "macos")]
|
||||
&PredefinedMenuItem::fullscreen(app_handle, None)?,
|
||||
],
|
||||
)?,
|
||||
&window_menu,
|
||||
&help_menu,
|
||||
#[cfg(dev)]
|
||||
&Submenu::with_items(
|
||||
app_handle,
|
||||
"Develop",
|
||||
true,
|
||||
&[
|
||||
&MenuItemBuilder::with_id("dev.refresh".to_string(), "Refresh")
|
||||
.accelerator("CmdOrCtrl+Shift+r")
|
||||
.build(app_handle)?,
|
||||
&MenuItemBuilder::with_id("dev.toggle_devtools".to_string(), "Open Devtools")
|
||||
.accelerator("CmdOrCtrl+Option+i")
|
||||
.build(app_handle)?,
|
||||
],
|
||||
)?,
|
||||
],
|
||||
)?;
|
||||
|
||||
Ok(menu)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"productName": "Yaak Proxy",
|
||||
"version": "0.0.0",
|
||||
"identifier": "app.yaak.proxy",
|
||||
"build": {
|
||||
"beforeBuildCommand": "npm --prefix ../.. run proxy:tauri-before-build",
|
||||
"beforeDevCommand": "npm --prefix ../.. run proxy:tauri-before-dev",
|
||||
"devUrl": "http://localhost:2420",
|
||||
"frontendDist": "../../dist/apps/yaak-proxy"
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": false,
|
||||
"windows": []
|
||||
},
|
||||
"bundle": {
|
||||
"icon": [
|
||||
"../yaak-app-client/icons/release/32x32.png",
|
||||
"../yaak-app-client/icons/release/128x128.png",
|
||||
"../yaak-app-client/icons/release/128x128@2x.png",
|
||||
"../yaak-app-client/icons/release/icon.icns",
|
||||
"../yaak-app-client/icons/release/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"productName": "Yaak Proxy Dev",
|
||||
"identifier": "app.yaak.proxy.dev",
|
||||
"bundle": {
|
||||
"icon": [
|
||||
"../yaak-app-client/icons/dev/32x32.png",
|
||||
"../yaak-app-client/icons/dev/128x128.png",
|
||||
"../yaak-app-client/icons/dev/128x128@2x.png",
|
||||
"../yaak-app-client/icons/dev/icon.icns",
|
||||
"../yaak-app-client/icons/dev/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"build": {
|
||||
"features": []
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Categories={{categories}}
|
||||
Comment={{comment}}
|
||||
Exec={{exec}}
|
||||
Icon={{icon}}
|
||||
Name={{name}}
|
||||
StartupWMClass={{exec}}
|
||||
Terminal=false
|
||||
Type=Application
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "yaak-app-client"
|
||||
name = "yaak-app"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
authors = ["Gregory Schier"]
|
||||
@@ -7,7 +7,7 @@ publish = false
|
||||
|
||||
# Produce a library for mobile support
|
||||
[lib]
|
||||
name = "tauri_app_client_lib"
|
||||
name = "tauri_app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "lib"]
|
||||
|
||||
[features]
|
||||
@@ -85,5 +85,4 @@ yaak-sse = { workspace = true }
|
||||
yaak-sync = { workspace = true }
|
||||
yaak-templates = { workspace = true }
|
||||
yaak-tls = { workspace = true }
|
||||
yaak-window = { workspace = true }
|
||||
yaak-ws = { workspace = true }
|
||||
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |