mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-02-19 14:17:53 +01:00
Compare commits
11 Commits
v2024.9.0
...
v2024.10.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99e91a692b | ||
|
|
24ea76e753 | ||
|
|
1d9a001036 | ||
|
|
7e056aa93a | ||
|
|
6247603cec | ||
|
|
a3b64423fd | ||
|
|
844d795014 | ||
|
|
d48b29c6e9 | ||
|
|
e0c00579af | ||
|
|
024edb6674 | ||
|
|
4a79a67b8d |
115
package-lock.json
generated
115
package-lock.json
generated
@@ -20,14 +20,14 @@
|
||||
"@react-hook/resize-observer": "^2.0.2",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tanstack/react-query": "^5.55.4",
|
||||
"@tauri-apps/api": "^2.0.0-rc.4",
|
||||
"@tauri-apps/api": "^2.0.0-rc.5",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.0.0-rc.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-fs": "^2.0.0-rc.2",
|
||||
"@tauri-apps/plugin-log": "^2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-os": "^2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-shell": "^2.0.0-rc.1",
|
||||
"@yaakapp/api": "^0.1.17",
|
||||
"@yaakapp/api": "^0.2.3",
|
||||
"buffer": "^6.0.3",
|
||||
"classnames": "^2.5.1",
|
||||
"cm6-graphql": "^0.0.9",
|
||||
@@ -59,7 +59,7 @@
|
||||
"devDependencies": {
|
||||
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
|
||||
"@tanstack/react-query-devtools": "^5.55.4",
|
||||
"@tauri-apps/cli": "^2.0.0-rc.14",
|
||||
"@tauri-apps/cli": "^2.0.0-rc.16",
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/papaparse": "^5.3.14",
|
||||
"@types/parse-color": "^1.0.3",
|
||||
@@ -90,7 +90,7 @@
|
||||
"rimraf": "^6.0.1",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"typescript": "^5.6.0",
|
||||
"vite": "^5.4.4",
|
||||
"vite": "^5.4.6",
|
||||
"vite-plugin-static-copy": "^1.0.6",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vite-plugin-top-level-await": "^1.4.4"
|
||||
@@ -2623,9 +2623,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/api": {
|
||||
"version": "2.0.0-rc.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-rc.4.tgz",
|
||||
"integrity": "sha512-UNiIhhKG08j4ooss2oEEVexffmWkgkYlC2M3GcX3VPtNsqFgVNL8Mcw/4Y7rO9M9S+ffAMnLOF5ypzyuyb8tyg==",
|
||||
"version": "2.0.0-rc.5",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-rc.5.tgz",
|
||||
"integrity": "sha512-JWs69pE9NsQdcqTpEVBepAZ08+jgQWuthIiTiaWRq/YlCLgnqq0KfC9sDem55uJ0YpgsytZuyj+m6b6q8oiD2g==",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -2633,9 +2633,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli": {
|
||||
"version": "2.0.0-rc.14",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.0.0-rc.14.tgz",
|
||||
"integrity": "sha512-AQST26ixCoVf32PSsSjfUCQY9SrawzDEgUpKXjelUeD9oROzbxlRw5tOYRTsxg9YDat2uWJ3xz7WXW6T0VEfNw==",
|
||||
"version": "2.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-wdjZg/M3dcxiqgWG6VRnABpX0dYxRww93t0d1MYoZxFDrnyoUz5kYwFQ0v4J9u0qenEgskjoypvon7V/Nj9qrg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"bin": {
|
||||
@@ -2649,22 +2649,22 @@
|
||||
"url": "https://opencollective.com/tauri"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tauri-apps/cli-darwin-arm64": "2.0.0-rc.14",
|
||||
"@tauri-apps/cli-darwin-x64": "2.0.0-rc.14",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "2.0.0-rc.14",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "2.0.0-rc.14",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "2.0.0-rc.14",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "2.0.0-rc.14",
|
||||
"@tauri-apps/cli-linux-x64-musl": "2.0.0-rc.14",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "2.0.0-rc.14",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "2.0.0-rc.14",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "2.0.0-rc.14"
|
||||
"@tauri-apps/cli-darwin-arm64": "2.0.0-rc.16",
|
||||
"@tauri-apps/cli-darwin-x64": "2.0.0-rc.16",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "2.0.0-rc.16",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "2.0.0-rc.16",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "2.0.0-rc.16",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "2.0.0-rc.16",
|
||||
"@tauri-apps/cli-linux-x64-musl": "2.0.0-rc.16",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "2.0.0-rc.16",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "2.0.0-rc.16",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "2.0.0-rc.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-arm64": {
|
||||
"version": "2.0.0-rc.14",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-rc.14.tgz",
|
||||
"integrity": "sha512-QBdzhwj+ewRi2tA+3jCiEMp5y9WYR3p33bwVHPC33oyrDFlXMNOpwsJa9VBHUU04pr6mq1ujrouHNBAlj78Lvw==",
|
||||
"version": "2.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-lISZU4gG0c9PbY7h/j/gW7nJLxZEygNBrYEET6zN8R99Znf5rSO+CfjenaMcJUUj6yTAd8gzdakRpLqNSAWegA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2679,9 +2679,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-x64": {
|
||||
"version": "2.0.0-rc.14",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.0-rc.14.tgz",
|
||||
"integrity": "sha512-m48s2+EIUYoq5T26CKq11TThRBlY5LorCmUKDO+OzIL87h5sXhYwAYKkskJ1aDc8gf3incq+cQhw2FfCkHk60A==",
|
||||
"version": "2.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-D9uxPCxpyYcTSQulJGFX3POAKPOJd8WcWHFH8x6YVM1cIx6EWRXIE1sZnPUOjFr7qCg+bSdYdr8/BFHcZGcApQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2696,9 +2696,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
|
||||
"version": "2.0.0-rc.14",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.0-rc.14.tgz",
|
||||
"integrity": "sha512-otGNUBERFQhuR+qYTTjMgoMUptc+EefyXsPQap7gFRAY6j5bBTiKOofE4XyOd6wFZ6OLI12uICm9j4kMjiPkzA==",
|
||||
"version": "2.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-WsVdKm4D1I1XV8I9yRnmOINZRwwWfh6xcLV3m19+B9g6TohK8RkRxWfxvs3TLQlWOLQ6lo8BzS9rzXB+KtjDpg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -2713,9 +2713,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
|
||||
"version": "2.0.0-rc.14",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.0-rc.14.tgz",
|
||||
"integrity": "sha512-ZCfd2N/IPQCEjrRyIeBK7nII//i34taAS6BbhDMuOXzBAz6r9angQKL42qW89mQDJZwckNHFiVMcOqBrrVo7TQ==",
|
||||
"version": "2.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-2jpZDagNs6rrqposHJihHBayttgOl5aB2+bYiVEC6ye+haiFtmYmpdaPOaVIw+YVQs6lepf5PVrisCoU9DmYsg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2730,9 +2730,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
|
||||
"version": "2.0.0-rc.14",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.0-rc.14.tgz",
|
||||
"integrity": "sha512-8zMZGVvehZSSn+TCt0D8uKXDqMviWYMNgfAc+XKWcbEpJKsAaDfC8UEFikpr1HBV/1kGrq0AMko+91f6o5/g6w==",
|
||||
"version": "2.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-SNEDcB+sWOls/B0a+UpUHVa/oegvlXXKYWsTxuXtgWIr5VbWG7rXLZ3fZpLLP3SpRVGTGTnABcyqshFbWSqqKQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2747,9 +2747,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
|
||||
"version": "2.0.0-rc.14",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.0-rc.14.tgz",
|
||||
"integrity": "sha512-NgEQ9f/Ce2SLDT0CNSpeew7BikTAOO3I2WM+FPuYyAaDnvrHnC67v3RyC5/j5xTkMMjRPsVPvsedtQNZjlq3tw==",
|
||||
"version": "2.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-Zsq29MM1ooeH1+chQBa7ffDFnzAZebBBFdsvs4e05tS1H8gn4oKE+PSMn9p/okzVXykEk9ri2/n7BG1XFeifMA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2764,9 +2764,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-musl": {
|
||||
"version": "2.0.0-rc.14",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.0-rc.14.tgz",
|
||||
"integrity": "sha512-h+/eJqWfwqzBXEJhmdNsYrUII7d9sz8UYWWS1cQFeCQRPW5rMVotySI7TCrmKOx0mZDKd9zWClYwUviBinG6Kg==",
|
||||
"version": "2.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-g+pwSuis2YMxhJJ/pJYwp/Nps5CWvlv/5MV5UfDvClkCkeAyzIqVX+HbBLPcs5S0CePUQNeP0j4d4jBWUqZZQg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2781,9 +2781,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
|
||||
"version": "2.0.0-rc.14",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.0.0-rc.14.tgz",
|
||||
"integrity": "sha512-bM8kGZJQCahTJQCdaF3h4ta83HvrIA8JEBUEHvrxxpiUpdtymOyW7uSSdjYB0pdmotZx44oEKiXeK2xcc9rZaw==",
|
||||
"version": "2.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-PpPqdMTwJSDAK4KnNjvh77ShSkY+7pih1f6e50EtXar8bjC17e3XcEqFhDNne5mxEVTLYhibs6p1JLPad0ZjRA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2798,9 +2798,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
|
||||
"version": "2.0.0-rc.14",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.0-rc.14.tgz",
|
||||
"integrity": "sha512-dxzPebtvI9EhqIzP33WbsPKoxyBejozw7Ie6GMpQ8oV2bogYfOgEu2r9hwzZ7nNfVldOD6PWKzgo0rwAvfxnAQ==",
|
||||
"version": "2.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-io2yIcEcG7YLP+9n13NbilB93SjcB7jIl8GbURC4XZT4/4t9D1PWHpJr5hySVsGRLCz5e8NzwC5RlnenNzmpPQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -2815,9 +2815,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
|
||||
"version": "2.0.0-rc.14",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.0-rc.14.tgz",
|
||||
"integrity": "sha512-XZ97TfOAL3KFr6pF51oWKZNXueFSmkpjkZdyk2/aVvzsiqGZtfPwGkL7YMCBbUA8q9ysaosdMvcGpBmvhHkLPQ==",
|
||||
"version": "2.0.0-rc.16",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.0-rc.16.tgz",
|
||||
"integrity": "sha512-Rfkmxe3k+cBVA/kVYt8O25QrQqWKJlH9AiH7Q3C6xBzzG9PCSRMBszCp+JhBF8jhVlwNmOBv6UG+lm85kspwGg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3322,11 +3322,11 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@yaakapp/api": {
|
||||
"version": "0.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@yaakapp/api/-/api-0.1.17.tgz",
|
||||
"integrity": "sha512-VE9A0FDZwczZkTAbMOYjQOKzbW1KmaItq/mPSuTgU87Lf570JUepcHVtL7QFLV1U/R5q+n7I6xQg9Q2mDj/OWQ==",
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@yaakapp/api/-/api-0.2.3.tgz",
|
||||
"integrity": "sha512-LKLk1EErWF0LyFj70yhZZzk2ZwwpC7xT3y3zPofgxUqKis9gW7lwevsTdyb1Acv18BY6IL2u8as7dzIN2p85ew==",
|
||||
"dependencies": {
|
||||
"@types/node": "^22.0.0"
|
||||
"@types/node": "^22.5.4"
|
||||
}
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
@@ -13601,11 +13601,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.5.tgz",
|
||||
"integrity": "sha512-pXqR0qtb2bTwLkev4SE3r4abCNioP3GkjvIDLlzziPpXtHgiJIjuKl+1GN6ESOT3wMjG3JTeARopj2SwYaHTOA==",
|
||||
"version": "5.4.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz",
|
||||
"integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
|
||||
@@ -39,14 +39,14 @@
|
||||
"@react-hook/resize-observer": "^2.0.2",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tanstack/react-query": "^5.55.4",
|
||||
"@tauri-apps/api": "^2.0.0-rc.4",
|
||||
"@tauri-apps/api": "^2.0.0-rc.5",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.0.0-rc.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-fs": "^2.0.0-rc.2",
|
||||
"@tauri-apps/plugin-log": "^2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-os": "^2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-shell": "^2.0.0-rc.1",
|
||||
"@yaakapp/api": "^0.1.17",
|
||||
"@yaakapp/api": "^0.2.3",
|
||||
"buffer": "^6.0.3",
|
||||
"classnames": "^2.5.1",
|
||||
"cm6-graphql": "^0.0.9",
|
||||
@@ -78,7 +78,7 @@
|
||||
"devDependencies": {
|
||||
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
|
||||
"@tanstack/react-query-devtools": "^5.55.4",
|
||||
"@tauri-apps/cli": "^2.0.0-rc.14",
|
||||
"@tauri-apps/cli": "^2.0.0-rc.16",
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/papaparse": "^5.3.14",
|
||||
"@types/parse-color": "^1.0.3",
|
||||
@@ -109,7 +109,7 @@
|
||||
"rimraf": "^6.0.1",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"typescript": "^5.6.0",
|
||||
"vite": "^5.4.4",
|
||||
"vite": "^5.4.6",
|
||||
"vite-plugin-static-copy": "^1.0.6",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vite-plugin-top-level-await": "^1.4.4"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@yaakapp/api",
|
||||
"version": "0.1.17",
|
||||
"version": "0.2.3",
|
||||
"main": "lib/index.js",
|
||||
"typings": "./lib/index.d.ts",
|
||||
"files": [
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type EmptyResponse = {};
|
||||
export type BootRequest = { dir: string, };
|
||||
@@ -1,3 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type PluginBootRequest = { dir: string, };
|
||||
export type BootResponse = { name: string, version: string, capabilities: Array<string>, };
|
||||
@@ -1,9 +1,10 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { BootRequest } from "./BootRequest";
|
||||
import type { BootResponse } from "./BootResponse";
|
||||
import type { CallHttpRequestActionRequest } from "./CallHttpRequestActionRequest";
|
||||
import type { CallTemplateFunctionRequest } from "./CallTemplateFunctionRequest";
|
||||
import type { CallTemplateFunctionResponse } from "./CallTemplateFunctionResponse";
|
||||
import type { CopyTextRequest } from "./CopyTextRequest";
|
||||
import type { EmptyResponse } from "./EmptyResponse";
|
||||
import type { ExportHttpRequestRequest } from "./ExportHttpRequestRequest";
|
||||
import type { ExportHttpRequestResponse } from "./ExportHttpRequestResponse";
|
||||
import type { FilterRequest } from "./FilterRequest";
|
||||
@@ -17,14 +18,10 @@ import type { GetHttpRequestByIdResponse } from "./GetHttpRequestByIdResponse";
|
||||
import type { GetTemplateFunctionsResponse } from "./GetTemplateFunctionsResponse";
|
||||
import type { ImportRequest } from "./ImportRequest";
|
||||
import type { ImportResponse } from "./ImportResponse";
|
||||
import type { PluginBootRequest } from "./PluginBootRequest";
|
||||
import type { PluginBootResponse } from "./PluginBootResponse";
|
||||
import type { PluginReloadRequest } from "./PluginReloadRequest";
|
||||
import type { PluginReloadResponse } from "./PluginReloadResponse";
|
||||
import type { RenderHttpRequestRequest } from "./RenderHttpRequestRequest";
|
||||
import type { RenderHttpRequestResponse } from "./RenderHttpRequestResponse";
|
||||
import type { SendHttpRequestRequest } from "./SendHttpRequestRequest";
|
||||
import type { SendHttpRequestResponse } from "./SendHttpRequestResponse";
|
||||
import type { ShowToastRequest } from "./ShowToastRequest";
|
||||
|
||||
export type InternalEventPayload = { "type": "boot_request" } & PluginBootRequest | { "type": "boot_response" } & PluginBootResponse | { "type": "reload_request" } & PluginReloadRequest | { "type": "reload_response" } & PluginReloadResponse | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & GetHttpRequestActionsRequest | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "copy_text_request" } & CopyTextRequest | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyResponse;
|
||||
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } | { "type": "reload_response" } | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & GetHttpRequestActionsRequest | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "copy_text_request" } & CopyTextRequest | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" };
|
||||
|
||||
@@ -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 PluginBootResponse = { name: string, version: string, capabilities: Array<string>, };
|
||||
@@ -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 PluginReloadRequest = {};
|
||||
@@ -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 PluginReloadResponse = {};
|
||||
@@ -12,7 +12,6 @@ export * from './gen/CookieDomain';
|
||||
export * from './gen/CookieExpires';
|
||||
export * from './gen/CookieJar';
|
||||
export * from './gen/CopyTextRequest';
|
||||
export * from './gen/EmptyResponse';
|
||||
export * from './gen/Environment';
|
||||
export * from './gen/EnvironmentVariable';
|
||||
export * from './gen/ExportHttpRequestRequest';
|
||||
@@ -43,10 +42,8 @@ export * from './gen/InternalEvent';
|
||||
export * from './gen/InternalEventPayload';
|
||||
export * from './gen/KeyValue';
|
||||
export * from './gen/Model';
|
||||
export * from './gen/PluginBootRequest';
|
||||
export * from './gen/PluginBootResponse';
|
||||
export * from './gen/PluginReloadRequest';
|
||||
export * from './gen/PluginReloadResponse';
|
||||
export * from './gen/BootRequest';
|
||||
export * from './gen/BootResponse';
|
||||
export * from './gen/RenderHttpRequestRequest';
|
||||
export * from './gen/RenderHttpRequestResponse';
|
||||
export * from './gen/RenderPurpose';
|
||||
|
||||
@@ -18,6 +18,10 @@ export class PluginHandle {
|
||||
this.#worker.postMessage(event);
|
||||
}
|
||||
|
||||
async terminate() {
|
||||
await this.#worker.terminate();
|
||||
}
|
||||
|
||||
#createWorker(): Worker {
|
||||
const workerPath = process.env.YAAK_WORKER_PATH ?? path.join(__dirname, 'index.worker.cjs');
|
||||
const worker = new Worker(workerPath, {
|
||||
|
||||
@@ -22,13 +22,19 @@ const plugins: Record<string, PluginHandle> = {};
|
||||
plugins[pluginEvent.pluginRefId] = plugin;
|
||||
}
|
||||
|
||||
// Once booted, forward all events to plugin's worker
|
||||
// Once booted, forward all events to the plugin worker
|
||||
const plugin = plugins[pluginEvent.pluginRefId];
|
||||
if (!plugin) {
|
||||
console.warn('Failed to get plugin for event by', pluginEvent.pluginRefId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pluginEvent.payload.type === 'terminate_request') {
|
||||
await plugin.terminate();
|
||||
console.log('Terminated plugin worker', pluginEvent.pluginRefId);
|
||||
delete plugins[pluginEvent.pluginRefId];
|
||||
}
|
||||
|
||||
plugin.sendToWorker(pluginEvent);
|
||||
}
|
||||
console.log('Stream ended');
|
||||
|
||||
@@ -155,6 +155,14 @@ async function initialize() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.type === 'terminate_request') {
|
||||
const payload: InternalEventPayload = {
|
||||
type: 'terminate_response',
|
||||
};
|
||||
sendPayload(payload, replyId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.type === 'import_request' && typeof mod.pluginHookImport === 'function') {
|
||||
const reply: ImportResponse | null = await mod.pluginHookImport(ctx, payload.content);
|
||||
if (reply != null) {
|
||||
@@ -313,8 +321,6 @@ function watchFile(filepath: string, cb: (filepath: string) => void) {
|
||||
const stat = statSync(filepath);
|
||||
if (stat.mtimeMs !== watchedFiles[filepath]?.mtimeMs) {
|
||||
cb(filepath);
|
||||
} else {
|
||||
console.log('SKIPPING SAME FILE STAT', filepath, stat);
|
||||
}
|
||||
watchedFiles[filepath] = stat;
|
||||
});
|
||||
|
||||
41
src-tauri/Cargo.lock
generated
41
src-tauri/Cargo.lock
generated
@@ -5773,13 +5773,12 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "2.0.0-rc.12"
|
||||
version = "2.0.0-rc.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65e5d4a319f11ae72c77d0f4dbd9703ab6b401eb91d1ca88d89c33d13d4ea20c"
|
||||
checksum = "eb3c3b1c7ac5b72d59da307b84af900a0098c74c9d7369f65018cd8ec0eb50fb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
"cocoa 0.26.0",
|
||||
"dirs",
|
||||
"dunce",
|
||||
"embed_plist",
|
||||
@@ -5795,7 +5794,9 @@ dependencies = [
|
||||
"log",
|
||||
"mime",
|
||||
"muda",
|
||||
"objc",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
"percent-encoding",
|
||||
"plist",
|
||||
"raw-window-handle",
|
||||
@@ -5824,9 +5825,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "2.0.0-rc.11"
|
||||
version = "2.0.0-rc.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "148441d64674b2885c1ba5baf3ae61662bb8753859ffcfb541962cbc6b847f39"
|
||||
checksum = "6ff5713e81e02e0b99f5219b275abbd7d2c0cc0f30180e25b1b650e08feeac63"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
@@ -5846,9 +5847,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-codegen"
|
||||
version = "2.0.0-rc.11"
|
||||
version = "2.0.0-rc.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72a15c3f9282c82871c69ddb65d02ae552738bbac848c8adcab521bf14d8b9e6"
|
||||
checksum = "5370f2591dcc93d4ff08d9dd168f5097f79b34e859883586a409c627544190e3"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"brotli",
|
||||
@@ -5873,9 +5874,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-macros"
|
||||
version = "2.0.0-rc.10"
|
||||
version = "2.0.0-rc.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f12d1aa317bec56f78388cf6012d788876d838595a48f95cbd7835642db356a0"
|
||||
checksum = "19442dc8ee002ab1926586f6aecb90114f3a1226766008b0c9ac2d9fec9eeb7e"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
@@ -6049,9 +6050,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-window-state"
|
||||
version = "2.0.0-rc.4"
|
||||
version = "2.0.0-rc.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67c9b841c8164b949eac9e4fe8d56a23ed5e8ef0ac1c52e26a500709f7a50e50"
|
||||
checksum = "2221224863eced96d800407f7ed8b8e6bd8f329e834b7b975bb553123fb79af0"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"log",
|
||||
@@ -6064,9 +6065,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.0.0-rc.11"
|
||||
version = "2.0.0-rc.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389f78c8e8e6eff3897d8d9581087943b5976ea96a0ab5036be691f28c2b0df0"
|
||||
checksum = "c5f38d8aaa1e81d20e8e208e3e317f81b59fb75c530fbae8a90e72d02001d687"
|
||||
dependencies = [
|
||||
"dpi",
|
||||
"gtk",
|
||||
@@ -6083,15 +6084,17 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime-wry"
|
||||
version = "2.0.0-rc.11"
|
||||
version = "2.0.0-rc.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "466c418aef2ddc7d31173a5b00bfae3df9b58ed66644339f3ca55008bfc54f25"
|
||||
checksum = "cf1ef5171e14c8fe3b5a63e75004c20d057747bc3e7fdc5f8ded625f0b29f5c7"
|
||||
dependencies = [
|
||||
"cocoa 0.26.0",
|
||||
"gtk",
|
||||
"http 1.1.0",
|
||||
"jni",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
"percent-encoding",
|
||||
"raw-window-handle",
|
||||
"softbuffer",
|
||||
@@ -6107,9 +6110,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-utils"
|
||||
version = "2.0.0-rc.11"
|
||||
version = "2.0.0-rc.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3019641087c9039b57ebfca95fa42a93c07056845b7d8d57c8966061bcee83b4"
|
||||
checksum = "31fe4c9148e1b35225e1c00753f24b517ce00041d02eb4b4d6fd10613a47736c"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"cargo_metadata",
|
||||
|
||||
@@ -16,7 +16,7 @@ crate-type = ["staticlib", "cdylib", "lib"]
|
||||
strip = true # Automatically strip symbols from the binary.
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.0-rc.0", features = [] }
|
||||
tauri-build = { version = "2.0.0-rc.12", features = [] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc = "0.2.7"
|
||||
@@ -52,7 +52,7 @@ tauri-plugin-fs = "2.0.0-rc.5"
|
||||
tauri-plugin-log = { version = "2.0.0-rc.2", features = ["colored"] }
|
||||
tauri-plugin-os = "2.0.0-rc.1"
|
||||
tauri-plugin-updater = "2.0.0-rc.3"
|
||||
tauri-plugin-window-state = "2.0.0-rc.4"
|
||||
tauri-plugin-window-state = "2.0.0-rc.5"
|
||||
tokio = { version = "1.36.0", features = ["sync"] }
|
||||
tokio-stream = "0.1.15"
|
||||
uuid = "1.7.0"
|
||||
@@ -64,4 +64,4 @@ urlencoding = "2.1.3"
|
||||
yaak_models = { path = "yaak_models" }
|
||||
yaak_plugin_runtime = { path = "yaak_plugin_runtime" }
|
||||
tauri-plugin-shell = "2.0.0-rc.3"
|
||||
tauri = { version = "2.0.0-rc.12", features = ["devtools", "protocol-asset"] }
|
||||
tauri = { version = "2.0.0-rc.15", features = ["devtools", "protocol-asset"] }
|
||||
|
||||
@@ -10,6 +10,7 @@ use std::time::Duration;
|
||||
use crate::render::render_http_request;
|
||||
use crate::response_err;
|
||||
use crate::template_callback::PluginTemplateCallback;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use base64::Engine;
|
||||
use http::header::{ACCEPT, USER_AGENT};
|
||||
use http::{HeaderMap, HeaderName, HeaderValue};
|
||||
@@ -204,7 +205,7 @@ pub async fn send_http_request<R: Runtime>(
|
||||
.unwrap_or_default();
|
||||
|
||||
let auth = format!("{username}:{password}");
|
||||
let encoded = base64::engine::general_purpose::STANDARD.encode(auth);
|
||||
let encoded = BASE64_STANDARD.encode(auth);
|
||||
headers.insert(
|
||||
"Authorization",
|
||||
HeaderValue::from_str(&format!("Basic {}", encoded)).unwrap(),
|
||||
@@ -497,6 +498,10 @@ fn replace_path_placeholder(p: &HttpUrlParameter, url: &str) -> String {
|
||||
if !p.enabled {
|
||||
return url.to_string();
|
||||
}
|
||||
|
||||
if !p.name.starts_with(":") {
|
||||
return url.to_string();
|
||||
}
|
||||
|
||||
let re = regex::Regex::new(format!("(/){}([/?#]|$)", p.name).as_str()).unwrap();
|
||||
let result = re
|
||||
|
||||
@@ -11,6 +11,7 @@ use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use base64::Engine;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use chrono::Utc;
|
||||
use fern::colors::ColoredLevelConfig;
|
||||
use log::{debug, error, info, warn};
|
||||
@@ -47,9 +48,9 @@ use yaak_models::queries::{
|
||||
cancel_pending_grpc_connections, cancel_pending_responses, create_default_http_response,
|
||||
delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment,
|
||||
delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request,
|
||||
delete_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request,
|
||||
generate_model_id, get_cookie_jar, get_environment, get_folder, get_grpc_connection,
|
||||
get_grpc_request, get_http_request, get_http_response, get_key_value_raw,
|
||||
delete_http_response, delete_plugin, delete_workspace, duplicate_grpc_request,
|
||||
duplicate_http_request, generate_model_id, get_cookie_jar, get_environment, get_folder,
|
||||
get_grpc_connection, get_grpc_request, get_http_request, get_http_response, get_key_value_raw,
|
||||
get_or_create_settings, get_plugin, get_workspace, list_cookie_jars, list_environments,
|
||||
list_folders, list_grpc_connections, list_grpc_events, list_grpc_requests, list_http_requests,
|
||||
list_http_responses, list_plugins, list_workspaces, set_key_value_raw, update_response_if_id,
|
||||
@@ -57,12 +58,12 @@ use yaak_models::queries::{
|
||||
upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_plugin, upsert_workspace,
|
||||
};
|
||||
use yaak_plugin_runtime::events::{
|
||||
CallHttpRequestActionRequest, FilterResponse, FindHttpResponsesResponse,
|
||||
BootResponse, CallHttpRequestActionRequest, FilterResponse, FindHttpResponsesResponse,
|
||||
GetHttpRequestActionsResponse, GetHttpRequestByIdResponse, GetTemplateFunctionsResponse,
|
||||
InternalEvent, InternalEventPayload, PluginBootResponse, RenderHttpRequestResponse,
|
||||
SendHttpRequestResponse, ShowToastRequest, ToastVariant,
|
||||
InternalEvent, InternalEventPayload, RenderHttpRequestResponse, SendHttpRequestResponse,
|
||||
ShowToastRequest, ToastVariant,
|
||||
};
|
||||
use yaak_plugin_runtime::handle::PluginHandle;
|
||||
use yaak_plugin_runtime::plugin_handle::PluginHandle;
|
||||
use yaak_templates::{Parser, Tokens};
|
||||
|
||||
mod analytics;
|
||||
@@ -82,6 +83,7 @@ const DEFAULT_WINDOW_HEIGHT: f64 = 600.0;
|
||||
|
||||
const MIN_WINDOW_WIDTH: f64 = 300.0;
|
||||
const MIN_WINDOW_HEIGHT: f64 = 300.0;
|
||||
const MAIN_WINDOW_PREFIX: &str = "main_";
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
@@ -241,7 +243,7 @@ async fn cmd_grpc_go(
|
||||
.unwrap_or("");
|
||||
|
||||
let auth = format!("{username}:{password}");
|
||||
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth);
|
||||
let encoded = BASE64_STANDARD.encode(auth);
|
||||
metadata.insert("Authorization".to_string(), format!("Basic {}", encoded));
|
||||
} else if b == "bearer" {
|
||||
let token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or("");
|
||||
@@ -1172,12 +1174,18 @@ async fn cmd_create_workspace(name: &str, w: WebviewWindow) -> Result<Workspace,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_create_plugin(
|
||||
async fn cmd_install_plugin(
|
||||
directory: &str,
|
||||
url: Option<String>,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
w: WebviewWindow,
|
||||
) -> Result<Plugin, String> {
|
||||
upsert_plugin(
|
||||
plugin_manager
|
||||
.add_plugin_by_dir(&directory)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let plugin = upsert_plugin(
|
||||
&w,
|
||||
Plugin {
|
||||
directory: directory.into(),
|
||||
@@ -1186,7 +1194,27 @@ async fn cmd_create_plugin(
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(plugin)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_uninstall_plugin(
|
||||
plugin_id: &str,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
w: WebviewWindow,
|
||||
) -> Result<Plugin, String> {
|
||||
let plugin = delete_plugin(&w, plugin_id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
plugin_manager
|
||||
.uninstall(plugin.directory.as_str())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(plugin)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -1463,8 +1491,14 @@ async fn cmd_list_plugins(w: WebviewWindow) -> Result<Vec<Plugin>, String> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_reload_plugins(plugin_manager: State<'_, PluginManager>) -> Result<(), String> {
|
||||
plugin_manager.reload_all().await;
|
||||
async fn cmd_reload_plugins(
|
||||
app_handle: AppHandle,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
) -> Result<(), String> {
|
||||
plugin_manager
|
||||
.initialize_all_plugins(&app_handle)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1473,12 +1507,15 @@ async fn cmd_plugin_info(
|
||||
id: &str,
|
||||
w: WebviewWindow,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
) -> Result<PluginBootResponse, String> {
|
||||
) -> Result<BootResponse, String> {
|
||||
let plugin = get_plugin(&w, id).await.map_err(|e| e.to_string())?;
|
||||
plugin_manager
|
||||
.get_plugin_info(plugin.directory.as_str())
|
||||
.get_plugin_by_dir(plugin.directory.as_str())
|
||||
.await
|
||||
.ok_or("Failed to find plugin info".to_string())
|
||||
.ok_or("Failed to find plugin info".to_string())?
|
||||
.info()
|
||||
.await
|
||||
.ok_or("Failed to find plugin".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -1719,8 +1756,7 @@ pub fn run() {
|
||||
let plugin_cb = PluginTemplateCallback::new(app.app_handle().clone());
|
||||
app.manage(plugin_cb);
|
||||
|
||||
let app_handle = app.app_handle().clone();
|
||||
monitor_plugin_events(&app_handle);
|
||||
monitor_plugin_events(&app.app_handle().clone());
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@@ -1732,7 +1768,7 @@ pub fn run() {
|
||||
cmd_create_folder,
|
||||
cmd_create_grpc_request,
|
||||
cmd_create_http_request,
|
||||
cmd_create_plugin,
|
||||
cmd_install_plugin,
|
||||
cmd_create_workspace,
|
||||
cmd_curl_to_request,
|
||||
cmd_delete_all_grpc_connections,
|
||||
@@ -1744,6 +1780,7 @@ pub fn run() {
|
||||
cmd_delete_grpc_request,
|
||||
cmd_delete_http_request,
|
||||
cmd_delete_http_response,
|
||||
cmd_uninstall_plugin,
|
||||
cmd_delete_workspace,
|
||||
cmd_dismiss_notification,
|
||||
cmd_duplicate_grpc_request,
|
||||
@@ -1909,7 +1946,7 @@ fn create_window(handle: &AppHandle, url: &str) -> WebviewWindow {
|
||||
handle.set_menu(menu).expect("Failed to set app menu");
|
||||
|
||||
let window_num = handle.webview_windows().len();
|
||||
let label = format!("main_{}", window_num);
|
||||
let label = format!("{MAIN_WINDOW_PREFIX}{window_num}");
|
||||
info!("Create new window label={label}");
|
||||
let mut win_builder =
|
||||
tauri::WebviewWindowBuilder::new(handle, label, WebviewUrl::App(url.into()))
|
||||
@@ -2005,14 +2042,20 @@ fn monitor_plugin_events<R: Runtime>(app_handle: &AppHandle<R>) {
|
||||
let app_handle = app_handle.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let plugin_manager: State<'_, PluginManager> = app_handle.state();
|
||||
let (_rx_id, mut rx) = plugin_manager.subscribe().await;
|
||||
let (rx_id, mut rx) = plugin_manager.subscribe().await;
|
||||
|
||||
while let Some(event) = rx.recv().await {
|
||||
let app_handle = app_handle.clone();
|
||||
let plugin = plugin_manager
|
||||
.get_plugin(event.plugin_ref_id.as_str())
|
||||
let plugin = match plugin_manager
|
||||
.get_plugin_by_ref_id(event.plugin_ref_id.as_str())
|
||||
.await
|
||||
.unwrap();
|
||||
{
|
||||
None => {
|
||||
warn!("Failed to get plugin for event {:?}", event);
|
||||
continue;
|
||||
}
|
||||
Some(p) => p,
|
||||
};
|
||||
|
||||
// We might have recursive back-and-forth calls between app and plugin, so we don't
|
||||
// want to block here
|
||||
@@ -2020,6 +2063,7 @@ fn monitor_plugin_events<R: Runtime>(app_handle: &AppHandle<R>) {
|
||||
handle_plugin_event(&app_handle, &event, &plugin).await;
|
||||
});
|
||||
}
|
||||
plugin_manager.unsubscribe(rx_id.as_str()).await;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2062,19 +2106,19 @@ async fn handle_plugin_event<R: Runtime>(
|
||||
))
|
||||
}
|
||||
InternalEventPayload::RenderHttpRequestRequest(req) => {
|
||||
let w = get_focused_window_no_lock(app_handle).expect("No focused window");
|
||||
let window = get_focused_window_no_lock(app_handle).expect("No focused window");
|
||||
let workspace = get_workspace(app_handle, req.http_request.workspace_id.as_str())
|
||||
.await
|
||||
.expect("Failed to get workspace for request");
|
||||
|
||||
let url = w.url().unwrap();
|
||||
let url = window.url().unwrap();
|
||||
let mut query_pairs = url.query_pairs();
|
||||
let environment_id = query_pairs
|
||||
.find(|(k, _v)| k == "environment_id")
|
||||
.map(|(_k, v)| v.to_string());
|
||||
let environment = match environment_id {
|
||||
None => None,
|
||||
Some(id) => get_environment(&w, id.as_str()).await.ok(),
|
||||
Some(id) => get_environment(&window, id.as_str()).await.ok(),
|
||||
};
|
||||
let cb = &*app_handle.state::<PluginTemplateCallback>();
|
||||
let rendered_http_request =
|
||||
@@ -2085,29 +2129,23 @@ async fn handle_plugin_event<R: Runtime>(
|
||||
},
|
||||
))
|
||||
}
|
||||
InternalEventPayload::ReloadResponse(_) => {
|
||||
let w = get_focused_window_no_lock(app_handle).expect("No focused window");
|
||||
let plugins = list_plugins(&w).await.unwrap();
|
||||
InternalEventPayload::ReloadResponse => {
|
||||
let window = get_focused_window_no_lock(app_handle).expect("No focused window");
|
||||
let plugins = list_plugins(&window).await.unwrap();
|
||||
for plugin in plugins {
|
||||
if plugin.directory != plugin_handle.dir {
|
||||
continue;
|
||||
}
|
||||
|
||||
upsert_plugin(
|
||||
&w,
|
||||
Plugin {
|
||||
// TODO: Add reloaded_at field to use instead
|
||||
updated_at: Utc::now().naive_utc(),
|
||||
..plugin
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let new_plugin = Plugin {
|
||||
updated_at: Utc::now().naive_utc(), // TODO: Add reloaded_at field to use instead
|
||||
..plugin
|
||||
};
|
||||
upsert_plugin(&window, new_plugin).await.unwrap();
|
||||
}
|
||||
let plugin_name = plugin_handle.info().await.unwrap().name;
|
||||
let toast_event = plugin_handle.build_event_to_send(
|
||||
&InternalEventPayload::ShowToastRequest(ShowToastRequest {
|
||||
message: format!("Reloaded plugin {}", plugin_name),
|
||||
message: format!("Reloaded plugin {}", plugin_handle.dir),
|
||||
variant: ToastVariant::Info,
|
||||
}),
|
||||
None,
|
||||
@@ -2174,18 +2212,25 @@ async fn handle_plugin_event<R: Runtime>(
|
||||
fn get_focused_window_no_lock<R: Runtime>(app_handle: &AppHandle<R>) -> Option<WebviewWindow<R>> {
|
||||
// TODO: Getting the focused window doesn't seem to work on Windows, so
|
||||
// we'll need to pass the window label into plugin events instead.
|
||||
if app_handle.webview_windows().len() == 1 {
|
||||
let w = app_handle
|
||||
.webview_windows()
|
||||
.iter()
|
||||
.next()
|
||||
.map(|w| w.1.clone());
|
||||
return w;
|
||||
}
|
||||
|
||||
app_handle
|
||||
let main_windows = app_handle
|
||||
.webview_windows()
|
||||
.iter()
|
||||
.find(|w| w.1.is_focused().unwrap_or(false))
|
||||
.map(|w| w.1.clone())
|
||||
.filter_map(|(_, w)| {
|
||||
if w.label().starts_with(MAIN_WINDOW_PREFIX) {
|
||||
Some(w.to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<WebviewWindow<R>>>();
|
||||
|
||||
if main_windows.len() == 1 {
|
||||
return main_windows.iter().next().map(|w| w.clone())
|
||||
}
|
||||
|
||||
main_windows
|
||||
.iter()
|
||||
.cloned()
|
||||
.find(|w| w.is_focused().unwrap_or(false))
|
||||
.map(|w| w.clone())
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ impl TemplateCallback for PluginTemplateCallback {
|
||||
} else {
|
||||
fn_name
|
||||
};
|
||||
|
||||
|
||||
let plugin_manager = self.app_handle.state::<PluginManager>();
|
||||
let function = plugin_manager
|
||||
.get_template_functions()
|
||||
@@ -46,7 +46,7 @@ impl TemplateCallback for PluginTemplateCallback {
|
||||
.ok_or("")?;
|
||||
|
||||
let mut args_with_defaults = args.clone();
|
||||
|
||||
|
||||
// Fill in default values for all args
|
||||
for a_def in function.args {
|
||||
let base = match a_def {
|
||||
|
||||
@@ -74,7 +74,7 @@ impl YaakUpdater {
|
||||
tauri::async_runtime::block_on(async move {
|
||||
info!("Shutting down plugin manager before update");
|
||||
let plugin_manager = h.state::<PluginManager>();
|
||||
plugin_manager.cleanup().await;
|
||||
plugin_manager.terminate().await;
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
@@ -954,6 +954,21 @@ pub async fn upsert_plugin<R: Runtime>(
|
||||
Ok(emit_upserted_model(window, m))
|
||||
}
|
||||
|
||||
pub async fn delete_plugin<R: Runtime>(window: &WebviewWindow<R>, id: &str) -> Result<Plugin> {
|
||||
let plugin = get_plugin(window, id).await?;
|
||||
|
||||
let dbm = &*window.app_handle().state::<SqliteConnection>();
|
||||
let db = dbm.0.lock().await.get().unwrap();
|
||||
|
||||
let (sql, params) = Query::delete()
|
||||
.from_table(PluginIden::Table)
|
||||
.cond_where(Expr::col(PluginIden::Id).eq(id))
|
||||
.build_rusqlite(SqliteQueryBuilder);
|
||||
db.execute(sql.as_str(), &*params.as_params())?;
|
||||
|
||||
emit_deleted_model(window, plugin)
|
||||
}
|
||||
|
||||
pub async fn get_folder<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<Folder> {
|
||||
let dbm = &*mgr.state::<SqliteConnection>();
|
||||
let db = dbm.0.lock().await.get().unwrap();
|
||||
|
||||
@@ -5,22 +5,35 @@ use crate::server::plugin_runtime::EventStreamEvent;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("IO error")]
|
||||
#[error("IO error: {0}")]
|
||||
IoErr(#[from] io::Error),
|
||||
#[error("Tauri error")]
|
||||
|
||||
#[error("Tauri error: {0}")]
|
||||
TauriErr(#[from] tauri::Error),
|
||||
#[error("Tauri shell error")]
|
||||
|
||||
#[error("Tauri shell error: {0}")]
|
||||
TauriShellErr(#[from] tauri_plugin_shell::Error),
|
||||
#[error("Grpc transport error")]
|
||||
|
||||
#[error("Grpc transport error: {0}")]
|
||||
GrpcTransportErr(#[from] tonic::transport::Error),
|
||||
#[error("Grpc send error")]
|
||||
|
||||
#[error("Grpc send error: {0}")]
|
||||
GrpcSendErr(#[from] SendError<tonic::Result<EventStreamEvent>>),
|
||||
#[error("JSON error")]
|
||||
|
||||
#[error("JSON error: {0}")]
|
||||
JsonErr(#[from] serde_json::Error),
|
||||
|
||||
#[error("Plugin not found: {0}")]
|
||||
PluginNotFoundErr(String),
|
||||
|
||||
#[error("Plugin error: {0}")]
|
||||
PluginErr(String),
|
||||
|
||||
#[error("Client not initialized error")]
|
||||
ClientNotInitializedErr,
|
||||
|
||||
#[error("Unknown event received")]
|
||||
UnknownEventErr,
|
||||
}
|
||||
|
||||
impl Into<String> for Error {
|
||||
|
||||
@@ -21,11 +21,14 @@ pub struct InternalEvent {
|
||||
#[serde(rename_all = "snake_case", tag = "type")]
|
||||
#[ts(export)]
|
||||
pub enum InternalEventPayload {
|
||||
BootRequest(PluginBootRequest),
|
||||
BootResponse(PluginBootResponse),
|
||||
BootRequest(BootRequest),
|
||||
BootResponse(BootResponse),
|
||||
|
||||
ReloadRequest(EmptyResponse),
|
||||
ReloadResponse(EmptyResponse),
|
||||
ReloadRequest,
|
||||
ReloadResponse,
|
||||
|
||||
TerminateRequest,
|
||||
TerminateResponse,
|
||||
|
||||
ImportRequest(ImportRequest),
|
||||
ImportResponse(ImportResponse),
|
||||
@@ -63,25 +66,20 @@ pub enum InternalEventPayload {
|
||||
|
||||
/// Returned when a plugin doesn't get run, just so the server
|
||||
/// has something to listen for
|
||||
EmptyResponse(EmptyResponse),
|
||||
EmptyResponse,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||
#[serde(default)]
|
||||
#[ts(export, type = "{}")]
|
||||
pub struct EmptyResponse {}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct PluginBootRequest {
|
||||
pub struct BootRequest {
|
||||
pub dir: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct PluginBootResponse {
|
||||
pub struct BootResponse {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub capabilities: Vec<String>,
|
||||
|
||||
@@ -4,5 +4,5 @@ pub mod manager;
|
||||
mod nodejs;
|
||||
pub mod plugin;
|
||||
mod server;
|
||||
pub mod handle;
|
||||
pub mod plugin_handle;
|
||||
mod util;
|
||||
|
||||
@@ -1,57 +1,236 @@
|
||||
use crate::error::Error::{ClientNotInitializedErr, PluginErr, PluginNotFoundErr, UnknownEventErr};
|
||||
use crate::error::Result;
|
||||
use crate::events::{
|
||||
PluginBootResponse, CallHttpRequestActionRequest, CallTemplateFunctionArgs,
|
||||
BootRequest, CallHttpRequestActionRequest, CallTemplateFunctionArgs,
|
||||
CallTemplateFunctionRequest, CallTemplateFunctionResponse, FilterRequest, FilterResponse,
|
||||
GetHttpRequestActionsRequest, GetHttpRequestActionsResponse, GetTemplateFunctionsResponse,
|
||||
ImportRequest, ImportResponse, InternalEvent, InternalEventPayload, RenderPurpose,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::error::Error::PluginErr;
|
||||
use crate::nodejs::start_nodejs_plugin_runtime;
|
||||
use crate::plugin::start_server;
|
||||
use crate::server::PluginRuntimeGrpcServer;
|
||||
use crate::plugin_handle::PluginHandle;
|
||||
use crate::server::plugin_runtime::plugin_runtime_server::PluginRuntimeServer;
|
||||
use crate::server::PluginRuntimeServerImpl;
|
||||
use log::{info, warn};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tauri::{AppHandle, Runtime};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::watch::Sender;
|
||||
use crate::handle::PluginHandle;
|
||||
use tauri::path::BaseDirectory;
|
||||
use tauri::{AppHandle, Manager, Runtime};
|
||||
use tokio::fs::read_dir;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use tonic::codegen::tokio_stream;
|
||||
use tonic::transport::Server;
|
||||
use yaak_models::queries::{generate_id, list_plugins};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PluginManager {
|
||||
kill_tx: Sender<bool>,
|
||||
server: PluginRuntimeGrpcServer,
|
||||
subscribers: Arc<Mutex<HashMap<String, mpsc::Sender<InternalEvent>>>>,
|
||||
plugins: Arc<Mutex<Vec<PluginHandle>>>,
|
||||
kill_tx: tokio::sync::watch::Sender<bool>,
|
||||
server: Arc<PluginRuntimeServerImpl>,
|
||||
}
|
||||
|
||||
impl PluginManager {
|
||||
pub async fn new<R: Runtime>(
|
||||
app_handle: &AppHandle<R>,
|
||||
plugin_dirs: Vec<String>,
|
||||
) -> PluginManager {
|
||||
let (server, addr) = start_server(plugin_dirs)
|
||||
.await
|
||||
.expect("Failed to start plugin runtime server");
|
||||
pub fn new<R: Runtime>(app_handle: AppHandle<R>) -> PluginManager {
|
||||
let (events_tx, mut events_rx) = mpsc::channel(128);
|
||||
let (kill_server_tx, kill_server_rx) = tokio::sync::watch::channel(false);
|
||||
|
||||
let (kill_tx, kill_rx) = tokio::sync::watch::channel(false);
|
||||
start_nodejs_plugin_runtime(app_handle, addr, &kill_rx)
|
||||
.await
|
||||
.expect("Failed to start plugin runtime");
|
||||
let (client_disconnect_tx, mut client_disconnect_rx) = mpsc::channel(128);
|
||||
let (client_connect_tx, mut client_connect_rx) = tokio::sync::watch::channel(false);
|
||||
let server =
|
||||
PluginRuntimeServerImpl::new(events_tx, client_disconnect_tx, client_connect_tx);
|
||||
|
||||
PluginManager { kill_tx, server }
|
||||
let plugin_manager = PluginManager {
|
||||
plugins: Arc::new(Mutex::new(Vec::new())),
|
||||
subscribers: Arc::new(Mutex::new(HashMap::new())),
|
||||
server: Arc::new(server.clone()),
|
||||
kill_tx: kill_server_tx,
|
||||
};
|
||||
|
||||
// Forward events to subscribers
|
||||
let subscribers = plugin_manager.subscribers.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
while let Some(event) = events_rx.recv().await {
|
||||
for (tx_id, tx) in subscribers.lock().await.iter_mut() {
|
||||
if let Err(e) = tx.try_send(event.clone()) {
|
||||
warn!("Failed to send event to subscriber {tx_id} {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle when client plugin runtime disconnects
|
||||
tauri::async_runtime::spawn(async move {
|
||||
while let Some(_) = client_disconnect_rx.recv().await {
|
||||
info!("Plugin runtime client disconnected! TODO: Handle this case");
|
||||
}
|
||||
});
|
||||
|
||||
info!("Starting plugin server");
|
||||
|
||||
let svc = PluginRuntimeServer::new(server.to_owned());
|
||||
let listen_addr = match option_env!("PORT") {
|
||||
None => "localhost:0".to_string(),
|
||||
Some(port) => format!("localhost:{port}"),
|
||||
};
|
||||
let listener = tauri::async_runtime::block_on(async move {
|
||||
TcpListener::bind(listen_addr)
|
||||
.await
|
||||
.expect("Failed to bind TCP listener")
|
||||
});
|
||||
let addr = listener.local_addr().expect("Failed to get local address");
|
||||
|
||||
// 1. Reload all plugins when the Node.js runtime connects
|
||||
{
|
||||
let plugin_manager = plugin_manager.clone();
|
||||
let app_handle = app_handle.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
match client_connect_rx.changed().await {
|
||||
Ok(_) => {
|
||||
info!("Plugin runtime client connected!");
|
||||
plugin_manager
|
||||
.initialize_all_plugins(&app_handle)
|
||||
.await
|
||||
.expect("Failed to reload plugins");
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to receive from client connection rx {e:?}");
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 1. Spawn server in the background
|
||||
info!("Starting gRPC plugin server on {addr}");
|
||||
tauri::async_runtime::spawn(async move {
|
||||
Server::builder()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.add_service(svc)
|
||||
.serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener))
|
||||
.await
|
||||
.expect("grpc plugin runtime server failed to start");
|
||||
});
|
||||
|
||||
// 2. Start Node.js runtime and initialize plugins
|
||||
tauri::async_runtime::block_on(async move {
|
||||
start_nodejs_plugin_runtime(&app_handle, addr, &kill_server_rx)
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
plugin_manager
|
||||
}
|
||||
|
||||
pub async fn reload_all(&self) {
|
||||
self.server.reload_plugins().await
|
||||
pub async fn list_plugin_dirs<R: Runtime>(&self, app_handle: &AppHandle<R>) -> Vec<String> {
|
||||
let plugins_dir = app_handle
|
||||
.path()
|
||||
.resolve("plugins", BaseDirectory::Resource)
|
||||
.expect("failed to resolve plugin directory resource");
|
||||
|
||||
let bundled_plugin_dirs = read_plugins_dir(&plugins_dir)
|
||||
.await
|
||||
.expect(format!("Failed to read plugins dir: {:?}", plugins_dir).as_str());
|
||||
|
||||
let plugins = list_plugins(app_handle).await.unwrap_or_default();
|
||||
let installed_plugin_dirs = plugins
|
||||
.iter()
|
||||
.map(|p| p.directory.to_owned())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let plugin_dirs = [bundled_plugin_dirs, installed_plugin_dirs].concat();
|
||||
plugin_dirs
|
||||
}
|
||||
|
||||
pub async fn uninstall(&self, dir: &str) -> Result<()> {
|
||||
let plugin = self
|
||||
.get_plugin_by_dir(dir)
|
||||
.await
|
||||
.ok_or(PluginNotFoundErr(dir.to_string()))?;
|
||||
self.remove_plugin(&plugin).await
|
||||
}
|
||||
|
||||
async fn remove_plugin(&self, plugin: &PluginHandle) -> Result<()> {
|
||||
let mut plugins = self.plugins.lock().await;
|
||||
|
||||
// Terminate the plugin
|
||||
plugin.terminate().await?;
|
||||
|
||||
// Remove the plugin from the list
|
||||
let pos = plugins.iter().position(|p| p.ref_id == plugin.ref_id);
|
||||
if let Some(pos) = pos {
|
||||
plugins.remove(pos);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_plugin_by_dir(&self, dir: &str) -> Result<()> {
|
||||
info!("Adding plugin by dir {dir}");
|
||||
let maybe_tx = self.server.app_to_plugin_events_tx.lock().await;
|
||||
let tx = match &*maybe_tx {
|
||||
None => return Err(ClientNotInitializedErr),
|
||||
Some(tx) => tx,
|
||||
};
|
||||
let ph = PluginHandle::new(dir, tx.clone());
|
||||
self.plugins.lock().await.push(ph.clone());
|
||||
let plugin = self
|
||||
.get_plugin_by_dir(dir)
|
||||
.await
|
||||
.ok_or(PluginNotFoundErr(dir.to_string()))?;
|
||||
|
||||
// Boot the plugin
|
||||
let event = self
|
||||
.send_to_plugin_and_wait(
|
||||
&plugin,
|
||||
&InternalEventPayload::BootRequest(BootRequest {
|
||||
dir: dir.to_string(),
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let resp = match event.payload {
|
||||
InternalEventPayload::BootResponse(resp) => resp,
|
||||
_ => return Err(UnknownEventErr),
|
||||
};
|
||||
|
||||
plugin.set_boot_response(&resp).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn initialize_all_plugins<R: Runtime>(
|
||||
&self,
|
||||
app_handle: &AppHandle<R>,
|
||||
) -> Result<()> {
|
||||
for dir in self.list_plugin_dirs(app_handle).await {
|
||||
// First remove the plugin if it exists
|
||||
if let Some(plugin) = self.get_plugin_by_dir(dir.as_str()).await {
|
||||
if let Err(e) = self.remove_plugin(&plugin).await {
|
||||
warn!("Failed to remove plugin {dir} {e:?}");
|
||||
}
|
||||
}
|
||||
if let Err(e) = self.add_plugin_by_dir(dir.as_str()).await {
|
||||
warn!("Failed to add plugin {dir} {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn subscribe(&self) -> (String, mpsc::Receiver<InternalEvent>) {
|
||||
self.server.subscribe().await
|
||||
let (tx, rx) = mpsc::channel(128);
|
||||
let rx_id = generate_id();
|
||||
self.subscribers.lock().await.insert(rx_id.clone(), tx);
|
||||
(rx_id, rx)
|
||||
}
|
||||
|
||||
pub async fn unsubscribe(&self, rx_id: &str) {
|
||||
self.server.unsubscribe(rx_id).await
|
||||
self.subscribers.lock().await.remove(rx_id);
|
||||
}
|
||||
|
||||
pub async fn cleanup(&self) {
|
||||
pub async fn terminate(&self) {
|
||||
self.kill_tx.send_replace(true);
|
||||
|
||||
// Give it a bit of time to kill
|
||||
@@ -64,22 +243,115 @@ impl PluginManager {
|
||||
payload: &InternalEventPayload,
|
||||
) -> Result<()> {
|
||||
let reply_id = Some(source_event.clone().id);
|
||||
self.server
|
||||
.send(&payload, source_event.plugin_ref_id.as_str(), reply_id)
|
||||
let plugin = self
|
||||
.get_plugin_by_ref_id(source_event.plugin_ref_id.as_str())
|
||||
.await
|
||||
.ok_or(PluginNotFoundErr(source_event.plugin_ref_id.to_string()))?;
|
||||
let event = plugin.build_event_to_send(&payload, reply_id);
|
||||
plugin.send(&event).await
|
||||
}
|
||||
|
||||
pub async fn get_plugin_by_ref_id(&self, ref_id: &str) -> Option<PluginHandle> {
|
||||
self.plugins
|
||||
.lock()
|
||||
.await
|
||||
.iter()
|
||||
.find(|p| p.ref_id == ref_id)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub async fn get_plugin_by_dir(&self, dir: &str) -> Option<PluginHandle> {
|
||||
self.plugins
|
||||
.lock()
|
||||
.await
|
||||
.iter()
|
||||
.find(|p| p.dir == dir)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub async fn get_plugin_by_name(&self, name: &str) -> Option<PluginHandle> {
|
||||
for plugin in self.plugins.lock().await.iter().cloned() {
|
||||
let info = plugin.info().await?;
|
||||
if info.name == name {
|
||||
return Some(plugin);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
async fn send_to_plugin_and_wait(
|
||||
&self,
|
||||
plugin: &PluginHandle,
|
||||
payload: &InternalEventPayload,
|
||||
) -> Result<InternalEvent> {
|
||||
let events = self
|
||||
.send_to_plugins_and_wait(payload, vec![plugin.to_owned()])
|
||||
.await?;
|
||||
Ok(events.first().unwrap().to_owned())
|
||||
}
|
||||
|
||||
async fn send_and_wait(&self, payload: &InternalEventPayload) -> Result<Vec<InternalEvent>> {
|
||||
self.send_to_plugins_and_wait(payload, self.plugins.lock().await.clone())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_plugin_info(&self, dir: &str) -> Option<PluginBootResponse> {
|
||||
self.server.plugin_by_dir(dir).await.ok()?.info().await
|
||||
}
|
||||
async fn send_to_plugins_and_wait(
|
||||
&self,
|
||||
payload: &InternalEventPayload,
|
||||
plugins: Vec<PluginHandle>,
|
||||
) -> Result<Vec<InternalEvent>> {
|
||||
let (rx_id, mut rx) = self.subscribe().await;
|
||||
|
||||
pub async fn get_plugin(&self, ref_id: &str) -> Result<PluginHandle> {
|
||||
self.server.plugin_by_ref_id(ref_id).await
|
||||
// 1. Build the events with IDs and everything
|
||||
let events_to_send = plugins
|
||||
.iter()
|
||||
.map(|p| p.build_event_to_send(payload, None))
|
||||
.collect::<Vec<InternalEvent>>();
|
||||
|
||||
// 2. Spawn thread to subscribe to incoming events and check reply ids
|
||||
let send_events_fut = {
|
||||
let events_to_send = events_to_send.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut found_events = Vec::new();
|
||||
|
||||
while let Some(event) = rx.recv().await {
|
||||
if events_to_send
|
||||
.iter()
|
||||
.find(|e| Some(e.id.to_owned()) == event.reply_id)
|
||||
.is_some()
|
||||
{
|
||||
found_events.push(event.clone());
|
||||
};
|
||||
if found_events.len() == events_to_send.len() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
found_events
|
||||
})
|
||||
};
|
||||
|
||||
// 3. Send the events
|
||||
for event in events_to_send {
|
||||
let plugin = plugins
|
||||
.iter()
|
||||
.find(|p| p.ref_id == event.plugin_ref_id)
|
||||
.expect("Didn't find plugin in list");
|
||||
plugin.send(&event).await?
|
||||
}
|
||||
|
||||
// 4. Join on the spawned thread
|
||||
let events = send_events_fut.await.expect("Thread didn't succeed");
|
||||
|
||||
// 5. Unsubscribe
|
||||
self.unsubscribe(rx_id.as_str()).await;
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
pub async fn get_http_request_actions(&self) -> Result<Vec<GetHttpRequestActionsResponse>> {
|
||||
let reply_events = self
|
||||
.server
|
||||
.send_and_wait(&InternalEventPayload::GetHttpRequestActionsRequest(
|
||||
GetHttpRequestActionsRequest {},
|
||||
))
|
||||
@@ -97,7 +369,6 @@ impl PluginManager {
|
||||
|
||||
pub async fn get_template_functions(&self) -> Result<Vec<GetTemplateFunctionsResponse>> {
|
||||
let reply_events = self
|
||||
.server
|
||||
.send_and_wait(&InternalEventPayload::GetTemplateFunctionsRequest)
|
||||
.await?;
|
||||
|
||||
@@ -112,10 +383,11 @@ impl PluginManager {
|
||||
}
|
||||
|
||||
pub async fn call_http_request_action(&self, req: CallHttpRequestActionRequest) -> Result<()> {
|
||||
let ref_id = req.plugin_ref_id.clone();
|
||||
let plugin = self
|
||||
.server
|
||||
.plugin_by_ref_id(req.plugin_ref_id.as_str())
|
||||
.await?;
|
||||
.get_plugin_by_ref_id(ref_id.as_str())
|
||||
.await
|
||||
.ok_or(PluginNotFoundErr(ref_id))?;
|
||||
let event = plugin.build_event_to_send(
|
||||
&InternalEventPayload::CallHttpRequestActionRequest(req),
|
||||
None,
|
||||
@@ -139,7 +411,6 @@ impl PluginManager {
|
||||
};
|
||||
|
||||
let events = self
|
||||
.server
|
||||
.send_and_wait(&InternalEventPayload::CallTemplateFunctionRequest(req))
|
||||
.await?;
|
||||
|
||||
@@ -155,7 +426,6 @@ impl PluginManager {
|
||||
|
||||
pub async fn import_data(&self, content: &str) -> Result<(ImportResponse, String)> {
|
||||
let reply_events = self
|
||||
.server
|
||||
.send_and_wait(&InternalEventPayload::ImportRequest(ImportRequest {
|
||||
content: content.to_string(),
|
||||
}))
|
||||
@@ -172,9 +442,12 @@ impl PluginManager {
|
||||
"No importers found for file contents".to_string(),
|
||||
)),
|
||||
Some((resp, ref_id)) => {
|
||||
let plugin = self.server.plugin_by_ref_id(ref_id.as_str()).await?;
|
||||
let plugin_name = plugin.name().await;
|
||||
Ok((resp, plugin_name))
|
||||
let plugin = self
|
||||
.get_plugin_by_ref_id(ref_id.as_str())
|
||||
.await
|
||||
.ok_or(PluginNotFoundErr(ref_id))?;
|
||||
let info = plugin.info().await.unwrap();
|
||||
Ok((resp, info.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,10 +464,14 @@ impl PluginManager {
|
||||
"filter-xpath"
|
||||
};
|
||||
|
||||
let plugin = self
|
||||
.get_plugin_by_dir(plugin_name)
|
||||
.await
|
||||
.ok_or(PluginNotFoundErr(plugin_name.to_string()))?;
|
||||
|
||||
let event = self
|
||||
.server
|
||||
.send_to_plugin_and_wait(
|
||||
plugin_name,
|
||||
&plugin,
|
||||
&InternalEventPayload::FilterRequest(FilterRequest {
|
||||
filter: filter.to_string(),
|
||||
content: content.to_string(),
|
||||
@@ -204,10 +481,46 @@ impl PluginManager {
|
||||
|
||||
match event.payload {
|
||||
InternalEventPayload::FilterResponse(resp) => Ok(resp),
|
||||
InternalEventPayload::EmptyResponse(_) => {
|
||||
InternalEventPayload::EmptyResponse => {
|
||||
Err(PluginErr("Filter returned empty".to_string()))
|
||||
}
|
||||
e => Err(PluginErr(format!("Export returned invalid event {:?}", e))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_plugins_dir(dir: &PathBuf) -> Result<Vec<String>> {
|
||||
let mut result = read_dir(dir).await?;
|
||||
let mut dirs: Vec<String> = vec![];
|
||||
while let Ok(Some(entry)) = result.next_entry().await {
|
||||
if entry.path().is_dir() {
|
||||
#[cfg(target_os = "windows")]
|
||||
dirs.push(fix_windows_paths(&entry.path()));
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
dirs.push(entry.path().to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
Ok(dirs)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn fix_windows_paths(p: &PathBuf) -> String {
|
||||
use dunce;
|
||||
use path_slash::PathBufExt;
|
||||
use regex::Regex;
|
||||
|
||||
// 1. Remove UNC prefix for Windows paths to pass to sidecar
|
||||
let safe_path = dunce::simplified(p.as_path()).to_string_lossy().to_string();
|
||||
|
||||
// 2. Remove the drive letter
|
||||
let safe_path = Regex::new("^[a-zA-Z]:")
|
||||
.unwrap()
|
||||
.replace(safe_path.as_str(), "");
|
||||
|
||||
// 3. Convert backslashes to forward
|
||||
let safe_path = PathBuf::from(safe_path.to_string())
|
||||
.to_slash_lossy()
|
||||
.to_string();
|
||||
|
||||
safe_path
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ pub async fn start_nodejs_plugin_runtime<R: Runtime>(
|
||||
.args(&[plugin_runtime_main]);
|
||||
|
||||
let (mut child_rx, child) = cmd.spawn()?;
|
||||
println!("Spawned plugin runtime");
|
||||
info!("Spawned plugin runtime");
|
||||
|
||||
let mut kill_rx = kill_rx.clone();
|
||||
|
||||
|
||||
@@ -1,46 +1,16 @@
|
||||
use crate::error::Result;
|
||||
use crate::events::{InternalEvent, InternalEventPayload};
|
||||
use crate::manager::PluginManager;
|
||||
use crate::server::plugin_runtime::plugin_runtime_server::PluginRuntimeServer;
|
||||
use crate::server::PluginRuntimeGrpcServer;
|
||||
use log::info;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::process::exit;
|
||||
use std::time::Duration;
|
||||
use tauri::path::BaseDirectory;
|
||||
use tauri::plugin::{Builder, TauriPlugin};
|
||||
use tauri::{Manager, RunEvent, Runtime, State};
|
||||
use tokio::fs::read_dir;
|
||||
use tokio::net::TcpListener;
|
||||
use tonic::codegen::tokio_stream;
|
||||
use tonic::transport::Server;
|
||||
use yaak_models::queries::list_plugins;
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("yaak_plugin_runtime")
|
||||
.setup(|app_handle, _| {
|
||||
let plugins_dir = app_handle
|
||||
.path()
|
||||
.resolve("plugins", BaseDirectory::Resource)
|
||||
.expect("failed to resolve plugin directory resource");
|
||||
let manager = PluginManager::new(app_handle.clone());
|
||||
app_handle.manage(manager.clone());
|
||||
|
||||
tauri::async_runtime::block_on(async move {
|
||||
let bundled_plugin_dirs = read_plugins_dir(&plugins_dir)
|
||||
.await
|
||||
.expect(format!("Failed to read plugins dir: {:?}", plugins_dir).as_str());
|
||||
|
||||
let plugins = list_plugins(app_handle).await.unwrap_or_default();
|
||||
let installed_plugin_dirs = plugins
|
||||
.iter()
|
||||
.map(|p| p.directory.to_owned())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let plugin_dirs = [installed_plugin_dirs, bundled_plugin_dirs].concat();
|
||||
let manager = PluginManager::new(&app_handle, plugin_dirs).await;
|
||||
app_handle.manage(manager);
|
||||
Ok(())
|
||||
})
|
||||
Ok(())
|
||||
})
|
||||
.on_event(|app, e| match e {
|
||||
// TODO: Also exit when app is force-quit (eg. cmd+r in IntelliJ runner)
|
||||
@@ -49,94 +19,11 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
tauri::async_runtime::block_on(async move {
|
||||
info!("Exiting plugin runtime due to app exit");
|
||||
let manager: State<PluginManager> = app.state();
|
||||
manager.cleanup().await;
|
||||
manager.terminate().await;
|
||||
exit(0);
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
pub async fn start_server(
|
||||
plugin_dirs: Vec<String>,
|
||||
) -> Result<(PluginRuntimeGrpcServer, SocketAddr)> {
|
||||
println!("Starting plugin server with {plugin_dirs:?}");
|
||||
let server = PluginRuntimeGrpcServer::new(plugin_dirs);
|
||||
|
||||
let svc = PluginRuntimeServer::new(server.clone());
|
||||
let listen_addr = match option_env!("PORT") {
|
||||
None => "localhost:0".to_string(),
|
||||
Some(port) => format!("localhost:{port}"),
|
||||
};
|
||||
|
||||
{
|
||||
let server = server.clone();
|
||||
tokio::spawn(async move {
|
||||
let (rx_id, mut rx) = server.subscribe().await;
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event.clone() {
|
||||
InternalEvent {
|
||||
payload: InternalEventPayload::BootResponse(resp),
|
||||
plugin_ref_id,
|
||||
..
|
||||
} => {
|
||||
server.boot_plugin(plugin_ref_id.as_str(), &resp).await;
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
server.unsubscribe(rx_id.as_str()).await;
|
||||
});
|
||||
};
|
||||
|
||||
let listener = TcpListener::bind(listen_addr).await?;
|
||||
let addr = listener.local_addr()?;
|
||||
println!("Starting gRPC plugin server on {addr}");
|
||||
tokio::spawn(async move {
|
||||
Server::builder()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.add_service(svc)
|
||||
.serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener))
|
||||
.await
|
||||
.expect("grpc plugin runtime server failed to start");
|
||||
});
|
||||
|
||||
Ok((server, addr))
|
||||
}
|
||||
|
||||
async fn read_plugins_dir(dir: &PathBuf) -> Result<Vec<String>> {
|
||||
let mut result = read_dir(dir).await?;
|
||||
let mut dirs: Vec<String> = vec![];
|
||||
while let Ok(Some(entry)) = result.next_entry().await {
|
||||
if entry.path().is_dir() {
|
||||
#[cfg(target_os = "windows")]
|
||||
dirs.push(fix_windows_paths(&entry.path()));
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
dirs.push(entry.path().to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
Ok(dirs)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn fix_windows_paths(p: &PathBuf) -> String {
|
||||
use dunce;
|
||||
use path_slash::PathBufExt;
|
||||
use regex::Regex;
|
||||
|
||||
// 1. Remove UNC prefix for Windows paths to pass to sidecar
|
||||
let safe_path = dunce::simplified(p.as_path()).to_string_lossy().to_string();
|
||||
|
||||
// 2. Remove the drive letter
|
||||
let safe_path = Regex::new("^[a-zA-Z]:")
|
||||
.unwrap()
|
||||
.replace(safe_path.as_str(), "");
|
||||
|
||||
// 3. Convert backslashes to forward
|
||||
let safe_path = PathBuf::from(safe_path.to_string())
|
||||
.to_slash_lossy()
|
||||
.to_string();
|
||||
|
||||
safe_path
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::events::{EmptyResponse, InternalEvent, InternalEventPayload, PluginBootResponse};
|
||||
use crate::error::Result;
|
||||
use crate::events::{BootResponse, InternalEvent, InternalEventPayload};
|
||||
use crate::server::plugin_runtime::EventStreamEvent;
|
||||
use crate::util::gen_id;
|
||||
use std::sync::Arc;
|
||||
use log::info;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -9,18 +11,22 @@ pub struct PluginHandle {
|
||||
pub ref_id: String,
|
||||
pub dir: String,
|
||||
pub(crate) to_plugin_tx: Arc<Mutex<mpsc::Sender<tonic::Result<EventStreamEvent>>>>,
|
||||
pub(crate) boot_resp: Arc<Mutex<Option<PluginBootResponse>>>,
|
||||
pub(crate) boot_resp: Arc<Mutex<Option<BootResponse>>>,
|
||||
}
|
||||
|
||||
impl PluginHandle {
|
||||
pub async fn name(&self) -> String {
|
||||
match &*self.boot_resp.lock().await {
|
||||
None => "__NOT_BOOTED__".to_string(),
|
||||
Some(r) => r.name.to_owned(),
|
||||
pub fn new(dir: &str, tx: mpsc::Sender<tonic::Result<EventStreamEvent>>) -> Self {
|
||||
let ref_id = gen_id();
|
||||
|
||||
PluginHandle {
|
||||
ref_id: ref_id.clone(),
|
||||
dir: dir.to_string(),
|
||||
to_plugin_tx: Arc::new(Mutex::new(tx)),
|
||||
boot_resp: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn info(&self) -> Option<PluginBootResponse> {
|
||||
pub async fn info(&self) -> Option<BootResponse> {
|
||||
let resp = &*self.boot_resp.lock().await;
|
||||
resp.clone()
|
||||
}
|
||||
@@ -38,28 +44,33 @@ impl PluginHandle {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn reload(&self) -> crate::error::Result<()> {
|
||||
let event = self.build_event_to_send(&InternalEventPayload::ReloadRequest(EmptyResponse{}), None);
|
||||
pub async fn terminate(&self) -> Result<()> {
|
||||
info!("Terminating plugin {}", self.dir);
|
||||
let event = self.build_event_to_send(&InternalEventPayload::TerminateRequest, None);
|
||||
self.send(&event).await
|
||||
}
|
||||
|
||||
pub async fn send(&self, event: &InternalEvent) -> crate::error::Result<()> {
|
||||
// info!(
|
||||
// "Sending event to plugin {} {:?}",
|
||||
// event.id,
|
||||
// self.name().await
|
||||
// );
|
||||
pub async fn send(&self, event: &InternalEvent) -> Result<()> {
|
||||
self.to_plugin_tx
|
||||
.lock()
|
||||
.await
|
||||
.send(Ok(EventStreamEvent {
|
||||
event: serde_json::to_string(&event)?,
|
||||
event: serde_json::to_string(event)?,
|
||||
}))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn boot(&self, resp: &PluginBootResponse) {
|
||||
pub async fn send_payload(
|
||||
&self,
|
||||
payload: &InternalEventPayload,
|
||||
reply_id: Option<String>,
|
||||
) -> Result<()> {
|
||||
let event = self.build_event_to_send(payload, reply_id);
|
||||
self.send(&event).await
|
||||
}
|
||||
|
||||
pub async fn set_boot_response(&self, resp: &BootResponse) {
|
||||
let mut boot_resp = self.boot_resp.lock().await;
|
||||
*boot_resp = Some(resp.clone());
|
||||
}
|
||||
@@ -1,293 +1,47 @@
|
||||
use std::collections::HashMap;
|
||||
use log::warn;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use log::warn;
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use tonic::codegen::tokio_stream::wrappers::ReceiverStream;
|
||||
use tonic::codegen::tokio_stream::{Stream, StreamExt};
|
||||
use tonic::{Request, Response, Status, Streaming};
|
||||
|
||||
use crate::error::Error::PluginNotFoundErr;
|
||||
use crate::error::Result;
|
||||
use crate::events::{InternalEvent, InternalEventPayload, PluginBootRequest, PluginBootResponse};
|
||||
use crate::handle::PluginHandle;
|
||||
use crate::events::InternalEvent;
|
||||
use crate::server::plugin_runtime::plugin_runtime_server::PluginRuntime;
|
||||
use crate::util::gen_id;
|
||||
use plugin_runtime::EventStreamEvent;
|
||||
use yaak_models::queries::generate_id;
|
||||
|
||||
pub mod plugin_runtime {
|
||||
tonic::include_proto!("yaak.plugins.runtime");
|
||||
}
|
||||
|
||||
type ResponseStream =
|
||||
Pin<Box<dyn Stream<Item = std::result::Result<EventStreamEvent, Status>> + Send>>;
|
||||
type ResponseStream = Pin<Box<dyn Stream<Item = Result<EventStreamEvent, Status>> + Send>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PluginRuntimeGrpcServer {
|
||||
plugin_ref_to_plugin: Arc<Mutex<HashMap<String, PluginHandle>>>,
|
||||
callback_to_plugin_ref: Arc<Mutex<HashMap<String, String>>>,
|
||||
subscribers: Arc<Mutex<HashMap<String, mpsc::Sender<InternalEvent>>>>,
|
||||
plugin_dirs: Vec<String>,
|
||||
pub(crate) struct PluginRuntimeServerImpl {
|
||||
pub(crate) app_to_plugin_events_tx:
|
||||
Arc<Mutex<Option<mpsc::Sender<tonic::Result<EventStreamEvent>>>>>,
|
||||
client_disconnect_tx: mpsc::Sender<bool>,
|
||||
client_connect_tx: tokio::sync::watch::Sender<bool>,
|
||||
plugin_to_app_events_tx: mpsc::Sender<InternalEvent>,
|
||||
}
|
||||
|
||||
impl PluginRuntimeGrpcServer {
|
||||
pub fn new(plugin_dirs: Vec<String>) -> Self {
|
||||
PluginRuntimeGrpcServer {
|
||||
plugin_ref_to_plugin: Arc::new(Mutex::new(HashMap::new())),
|
||||
callback_to_plugin_ref: Arc::new(Mutex::new(HashMap::new())),
|
||||
subscribers: Arc::new(Mutex::new(HashMap::new())),
|
||||
plugin_dirs,
|
||||
impl PluginRuntimeServerImpl {
|
||||
pub fn new(
|
||||
events_tx: mpsc::Sender<InternalEvent>,
|
||||
disconnect_tx: mpsc::Sender<bool>,
|
||||
connect_tx: tokio::sync::watch::Sender<bool>,
|
||||
) -> Self {
|
||||
PluginRuntimeServerImpl {
|
||||
app_to_plugin_events_tx: Arc::new(Mutex::new(None)),
|
||||
client_disconnect_tx: disconnect_tx,
|
||||
client_connect_tx: connect_tx,
|
||||
plugin_to_app_events_tx: events_tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn plugins(&self) -> Vec<PluginHandle> {
|
||||
self.plugin_ref_to_plugin
|
||||
.lock()
|
||||
.await
|
||||
.iter()
|
||||
.map(|p| p.1.to_owned())
|
||||
.collect::<Vec<PluginHandle>>()
|
||||
}
|
||||
|
||||
pub async fn subscribe(&self) -> (String, Receiver<InternalEvent>) {
|
||||
let (tx, rx) = mpsc::channel(128);
|
||||
let rx_id = generate_id();
|
||||
self.subscribers.lock().await.insert(rx_id.clone(), tx);
|
||||
(rx_id, rx)
|
||||
}
|
||||
|
||||
pub async fn unsubscribe(&self, rx_id: &str) {
|
||||
self.subscribers.lock().await.remove(rx_id);
|
||||
}
|
||||
|
||||
pub async fn remove_plugins(&self, plugin_ids: Vec<String>) {
|
||||
for plugin_id in plugin_ids {
|
||||
self.remove_plugin(plugin_id.as_str()).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn remove_plugin(&self, id: &str) {
|
||||
match self.plugin_ref_to_plugin.lock().await.remove(id) {
|
||||
None => println!("Tried to remove non-existing plugin {}", id),
|
||||
Some(plugin) => println!("Removed plugin {} {}", id, plugin.name().await),
|
||||
};
|
||||
}
|
||||
|
||||
pub async fn boot_plugin(&self, id: &str, resp: &PluginBootResponse) {
|
||||
match self.plugin_ref_to_plugin.lock().await.get(id) {
|
||||
None => println!("Tried booting non-existing plugin {}", id),
|
||||
Some(plugin) => plugin.clone().boot(resp).await,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn add_plugin(
|
||||
&self,
|
||||
dir: &str,
|
||||
tx: mpsc::Sender<tonic::Result<EventStreamEvent>>,
|
||||
) -> PluginHandle {
|
||||
let ref_id = gen_id();
|
||||
let plugin_handle = PluginHandle {
|
||||
ref_id: ref_id.clone(),
|
||||
dir: dir.to_string(),
|
||||
to_plugin_tx: Arc::new(Mutex::new(tx)),
|
||||
boot_resp: Arc::new(Mutex::new(None)),
|
||||
};
|
||||
let _ = self
|
||||
.plugin_ref_to_plugin
|
||||
.lock()
|
||||
.await
|
||||
.insert(ref_id, plugin_handle.clone());
|
||||
plugin_handle
|
||||
}
|
||||
|
||||
pub async fn plugin_by_ref_id(&self, ref_id: &str) -> Result<PluginHandle> {
|
||||
let plugins = self.plugin_ref_to_plugin.lock().await;
|
||||
match plugins.get(ref_id) {
|
||||
None => Err(PluginNotFoundErr(ref_id.into())),
|
||||
Some(p) => Ok(p.to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn plugin_by_dir(&self, dir: &str) -> Result<PluginHandle> {
|
||||
let plugins = self.plugin_ref_to_plugin.lock().await;
|
||||
for p in plugins.values() {
|
||||
if p.dir == dir {
|
||||
return Ok(p.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
Err(PluginNotFoundErr(dir.into()))
|
||||
}
|
||||
|
||||
pub async fn plugin_by_name(&self, plugin_name: &str) -> Result<PluginHandle> {
|
||||
let plugins = self.plugin_ref_to_plugin.lock().await;
|
||||
for p in plugins.values() {
|
||||
if p.name().await == plugin_name {
|
||||
return Ok(p.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
Err(PluginNotFoundErr(plugin_name.into()))
|
||||
}
|
||||
|
||||
pub async fn send(
|
||||
&self,
|
||||
payload: &InternalEventPayload,
|
||||
plugin_ref_id: &str,
|
||||
reply_id: Option<String>,
|
||||
) -> Result<()> {
|
||||
let plugin = self.plugin_by_ref_id(plugin_ref_id).await?;
|
||||
let event = plugin.build_event_to_send(payload, reply_id);
|
||||
plugin.send(&event).await
|
||||
}
|
||||
|
||||
pub async fn send_to_plugin(
|
||||
&self,
|
||||
plugin_name: &str,
|
||||
payload: InternalEventPayload,
|
||||
) -> Result<InternalEvent> {
|
||||
let plugins = self.plugin_ref_to_plugin.lock().await;
|
||||
if plugins.is_empty() {
|
||||
return Err(PluginNotFoundErr(plugin_name.into()));
|
||||
}
|
||||
|
||||
let mut plugin = None;
|
||||
for p in plugins.values() {
|
||||
if p.name().await == plugin_name {
|
||||
plugin = Some(p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
match plugin {
|
||||
Some(plugin) => {
|
||||
let event = plugin.build_event_to_send(&payload, None);
|
||||
plugin.send(&event).await?;
|
||||
Ok(event)
|
||||
}
|
||||
None => Err(PluginNotFoundErr(plugin_name.into())),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_to_plugin_and_wait(
|
||||
&self,
|
||||
plugin_name: &str,
|
||||
payload: &InternalEventPayload,
|
||||
) -> Result<InternalEvent> {
|
||||
let plugin = self.plugin_by_name(plugin_name).await?;
|
||||
let events = self.send_to_plugins_and_wait(payload, vec![plugin]).await?;
|
||||
Ok(events.first().unwrap().to_owned())
|
||||
}
|
||||
|
||||
pub async fn send_and_wait(
|
||||
&self,
|
||||
payload: &InternalEventPayload,
|
||||
) -> Result<Vec<InternalEvent>> {
|
||||
let plugins = self
|
||||
.plugin_ref_to_plugin
|
||||
.lock()
|
||||
.await
|
||||
.values()
|
||||
.cloned()
|
||||
.collect();
|
||||
self.send_to_plugins_and_wait(payload, plugins).await
|
||||
}
|
||||
|
||||
async fn send_to_plugins_and_wait(
|
||||
&self,
|
||||
payload: &InternalEventPayload,
|
||||
plugins: Vec<PluginHandle>,
|
||||
) -> Result<Vec<InternalEvent>> {
|
||||
// 1. Build the events with IDs and everything
|
||||
let events_to_send = plugins
|
||||
.iter()
|
||||
.map(|p| p.build_event_to_send(payload, None))
|
||||
.collect::<Vec<InternalEvent>>();
|
||||
|
||||
// 2. Spawn thread to subscribe to incoming events and check reply ids
|
||||
let server = self.clone();
|
||||
let send_events_fut = {
|
||||
let events_to_send = events_to_send.clone();
|
||||
tokio::spawn(async move {
|
||||
let (rx_id, mut rx) = server.subscribe().await;
|
||||
let mut found_events = Vec::new();
|
||||
|
||||
while let Some(event) = rx.recv().await {
|
||||
if events_to_send
|
||||
.iter()
|
||||
.find(|e| Some(e.id.to_owned()) == event.reply_id)
|
||||
.is_some()
|
||||
{
|
||||
found_events.push(event.clone());
|
||||
};
|
||||
if found_events.len() == events_to_send.len() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
server.unsubscribe(rx_id.as_str()).await;
|
||||
|
||||
found_events
|
||||
})
|
||||
};
|
||||
|
||||
// 3. Send the events
|
||||
for event in events_to_send {
|
||||
let plugin = plugins
|
||||
.iter()
|
||||
.find(|p| p.ref_id == event.plugin_ref_id)
|
||||
.expect("Didn't find plugin in list");
|
||||
plugin.send(&event).await?
|
||||
}
|
||||
|
||||
// 4. Join on the spawned thread
|
||||
let events = send_events_fut.await.expect("Thread didn't succeed");
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
pub async fn reload_plugins(&self) {
|
||||
for (_, plugin) in self.plugin_ref_to_plugin.lock().await.clone() {
|
||||
if let Err(e) = plugin.reload().await {
|
||||
warn!("Failed to reload plugin {} {}", plugin.dir, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_plugins(
|
||||
&self,
|
||||
to_plugin_tx: mpsc::Sender<tonic::Result<EventStreamEvent>>,
|
||||
plugin_dirs: Vec<String>,
|
||||
) -> Vec<String> {
|
||||
let mut plugin_ids = Vec::new();
|
||||
|
||||
for dir in plugin_dirs {
|
||||
let plugin = self.add_plugin(dir.as_str(), to_plugin_tx.clone()).await;
|
||||
plugin_ids.push(plugin.clone().ref_id);
|
||||
|
||||
let event = plugin.build_event_to_send(
|
||||
&InternalEventPayload::BootRequest(PluginBootRequest {
|
||||
dir: dir.to_string(),
|
||||
}),
|
||||
None,
|
||||
);
|
||||
if let Err(e) = plugin.send(&event).await {
|
||||
// TODO: Error handling
|
||||
println!(
|
||||
"Failed boot plugin {} at {} -> {}",
|
||||
plugin.ref_id, plugin.dir, e
|
||||
)
|
||||
} else {
|
||||
println!("Loaded plugin {} at {}", plugin.ref_id, plugin.dir)
|
||||
}
|
||||
}
|
||||
|
||||
plugin_ids
|
||||
}
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl PluginRuntime for PluginRuntimeGrpcServer {
|
||||
impl PluginRuntime for PluginRuntimeServerImpl {
|
||||
type EventStreamStream = ResponseStream;
|
||||
|
||||
async fn event_stream(
|
||||
@@ -296,51 +50,48 @@ impl PluginRuntime for PluginRuntimeGrpcServer {
|
||||
) -> tonic::Result<Response<Self::EventStreamStream>> {
|
||||
let mut in_stream = req.into_inner();
|
||||
|
||||
let (to_plugin_tx, to_plugin_rx) = mpsc::channel(128);
|
||||
let (to_plugin_tx, to_plugin_rx) = mpsc::channel::<tonic::Result<EventStreamEvent>>(128);
|
||||
let mut app_to_plugin_events_tx = self.app_to_plugin_events_tx.lock().await;
|
||||
*app_to_plugin_events_tx = Some(to_plugin_tx);
|
||||
println!("GRPC CLIENT CONNECTED");
|
||||
|
||||
let plugin_ids = self
|
||||
.load_plugins(to_plugin_tx, self.plugin_dirs.clone())
|
||||
.await;
|
||||
let plugin_to_app_events_tx = self.plugin_to_app_events_tx.clone();
|
||||
let client_disconnect_tx = self.client_disconnect_tx.clone();
|
||||
|
||||
self.client_connect_tx
|
||||
.send(true)
|
||||
.expect("Failed to send client ready event");
|
||||
|
||||
let callbacks = self.callback_to_plugin_ref.clone();
|
||||
let server = self.clone();
|
||||
tokio::spawn(async move {
|
||||
while let Some(result) = in_stream.next().await {
|
||||
// Received event from plugin runtime
|
||||
match result {
|
||||
Ok(v) => {
|
||||
let event: InternalEvent = match serde_json::from_str(v.event.as_str()) {
|
||||
Ok(pe) => pe,
|
||||
Err(e) => {
|
||||
println!("Failed to deserialize event {e:?} -> {}", v.event);
|
||||
warn!("Failed to deserialize event {e:?} -> {}", v.event);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let plugin_ref_id = event.plugin_ref_id.clone();
|
||||
let reply_id = event.reply_id.clone();
|
||||
|
||||
let subscribers = server.subscribers.lock().await;
|
||||
for tx in subscribers.values() {
|
||||
// Emit event to the channel for server to handle
|
||||
if let Err(e) = tx.try_send(event.clone()) {
|
||||
println!("Failed to send to server channel (n={}). Receiver probably isn't listening: {:?}", subscribers.len(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add to callbacks if there's a reply_id
|
||||
if let Some(reply_id) = reply_id {
|
||||
callbacks.lock().await.insert(reply_id, plugin_ref_id);
|
||||
// Send event to subscribers
|
||||
// Emit event to the channel for server to handle
|
||||
if let Err(e) = plugin_to_app_events_tx.try_send(event.clone()) {
|
||||
warn!("Failed to send to channel. Receiver probably isn't listening: {:?}", e);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
// TODO: Better error handling
|
||||
println!("gRPC server error {err}");
|
||||
warn!("gRPC server error {err}");
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
server.remove_plugins(plugin_ids).await;
|
||||
if let Err(e) = client_disconnect_tx.send(true).await {
|
||||
warn!("Failed to send killed event {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// Write the same data that was received
|
||||
|
||||
@@ -195,7 +195,7 @@ impl Parser {
|
||||
fn parse_fn(&mut self) -> Option<(String, Vec<FnArg>)> {
|
||||
let start_pos = self.pos;
|
||||
|
||||
let name = match self.parse_ident() {
|
||||
let name = match self.parse_fn_name() {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
self.pos = start_pos;
|
||||
@@ -292,6 +292,32 @@ impl Parser {
|
||||
|
||||
Some(text)
|
||||
}
|
||||
|
||||
fn parse_fn_name(&mut self) -> Option<String> {
|
||||
let start_pos = self.pos;
|
||||
|
||||
let mut text = String::new();
|
||||
while self.pos < self.chars.len() {
|
||||
let ch = self.peek_char();
|
||||
if ch.is_alphanumeric() || ch == '_' || ch == '.' {
|
||||
text.push(ch);
|
||||
self.pos += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
if start_pos == self.pos {
|
||||
panic!("Parser stuck!");
|
||||
}
|
||||
}
|
||||
|
||||
if text.is_empty() {
|
||||
self.pos = start_pos;
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(text)
|
||||
}
|
||||
|
||||
fn parse_string(&mut self) -> Option<String> {
|
||||
let start_pos = self.pos;
|
||||
@@ -486,6 +512,23 @@ mod tests {
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_dot_name() {
|
||||
let mut p = Parser::new("${[ foo.bar.baz() ]}");
|
||||
assert_eq!(
|
||||
p.parse().tokens,
|
||||
vec![
|
||||
Token::Tag {
|
||||
val: Val::Fn {
|
||||
name: "foo.bar.baz".into(),
|
||||
args: Vec::new(),
|
||||
}
|
||||
},
|
||||
Token::Eof
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_ident_arg() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||
import classNames from 'classnames';
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import { useIsFullscreen } from '../hooks/useIsFullscreen';
|
||||
@@ -24,6 +25,10 @@ export function HeaderSize({
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
onDoubleClick={async () => {
|
||||
// Maximize window on double-click
|
||||
await getCurrentWebviewWindow().toggleMaximize();
|
||||
}}
|
||||
style={{
|
||||
...style,
|
||||
// Add padding for macOS stoplights, but keep it the same width (account for the interface scale)
|
||||
|
||||
@@ -117,6 +117,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
|
||||
>
|
||||
{activeResponse && (
|
||||
<HStack
|
||||
as="p"
|
||||
space={2}
|
||||
className="whitespace-nowrap w-full pl-3 overflow-x-auto font-mono text-sm"
|
||||
>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { Plugin } from '@yaakapp/api';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import React from 'react';
|
||||
import { useCreatePlugin } from '../../hooks/useCreatePlugin';
|
||||
import { useInstallPlugin } from '../../hooks/useInstallPlugin';
|
||||
import { useUninstallPlugin } from '../../hooks/useUninstallPlugin';
|
||||
import { usePluginInfo } from '../../hooks/usePluginInfo';
|
||||
import { usePlugins, useRefreshPlugins } from '../../hooks/usePlugins';
|
||||
import { Button } from '../core/Button';
|
||||
import { Checkbox } from '../core/Checkbox';
|
||||
import { IconButton } from '../core/IconButton';
|
||||
import { InlineCode } from '../core/InlineCode';
|
||||
import { HStack } from '../core/Stacks';
|
||||
@@ -15,7 +15,7 @@ import { SelectFile } from '../SelectFile';
|
||||
export function SettingsPlugins() {
|
||||
const [directory, setDirectory] = React.useState<string | null>(null);
|
||||
const plugins = usePlugins();
|
||||
const createPlugin = useCreatePlugin();
|
||||
const createPlugin = useInstallPlugin();
|
||||
const refreshPlugins = useRefreshPlugins();
|
||||
return (
|
||||
<div className="grid grid-rows-[minmax(0,1fr)_auto] h-full">
|
||||
@@ -31,7 +31,6 @@ export function SettingsPlugins() {
|
||||
<table className="w-full text-sm mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th className="py-2 text-left">Plugin</th>
|
||||
<th className="py-2 text-right">Version</th>
|
||||
<th></th>
|
||||
@@ -88,14 +87,10 @@ export function SettingsPlugins() {
|
||||
|
||||
function PluginInfo({ plugin }: { plugin: Plugin }) {
|
||||
const pluginInfo = usePluginInfo(plugin.id);
|
||||
const deletePlugin = useUninstallPlugin(plugin.id);
|
||||
return (
|
||||
<tr className="group">
|
||||
<td className="pr-2">
|
||||
<Checkbox hideLabel checked={true} title="foo" onChange={() => null} />
|
||||
</td>
|
||||
<td className="py-2 select-text cursor-text w-full">
|
||||
<InlineCode>{pluginInfo.data?.name}</InlineCode>
|
||||
</td>
|
||||
<td className="py-2 select-text cursor-text w-full">{pluginInfo.data?.name}</td>
|
||||
<td className="py-2 select-text cursor-text text-right">
|
||||
<InlineCode>{pluginInfo.data?.version}</InlineCode>
|
||||
</td>
|
||||
@@ -105,6 +100,7 @@ function PluginInfo({ plugin }: { plugin: Plugin }) {
|
||||
icon="trash"
|
||||
title="Uninstall plugin"
|
||||
className="text-text-subtlest"
|
||||
onClick={() => deletePlugin.mutate()}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -242,6 +242,11 @@
|
||||
@apply text-primary;
|
||||
}
|
||||
|
||||
&.cm-completionIcon-namespace::after {
|
||||
content: 'n' !important;
|
||||
@apply text-warning;
|
||||
}
|
||||
|
||||
&.cm-completionIcon-constant::after {
|
||||
content: 'c' !important;
|
||||
@apply text-notice;
|
||||
@@ -267,10 +272,6 @@
|
||||
content: 'm' !important;
|
||||
}
|
||||
|
||||
&.cm-completionIcon-namespace::after {
|
||||
content: 'n' !important;
|
||||
}
|
||||
|
||||
&.cm-completionIcon-property::after {
|
||||
content: 'a' !important;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CompletionContext } from '@codemirror/autocomplete';
|
||||
import type { Completion, CompletionContext } from '@codemirror/autocomplete';
|
||||
|
||||
const openTag = '${[ ';
|
||||
const closeTag = ' ]}';
|
||||
@@ -7,12 +7,20 @@ export type TwigCompletionOptionVariable = {
|
||||
type: 'variable';
|
||||
};
|
||||
|
||||
export type TwigCompletionOptionNamespace = {
|
||||
type: 'namespace';
|
||||
};
|
||||
|
||||
export type TwigCompletionOptionFunction = {
|
||||
args: { name: string }[];
|
||||
type: 'function';
|
||||
};
|
||||
|
||||
export type TwigCompletionOption = (TwigCompletionOptionFunction | TwigCompletionOptionVariable) & {
|
||||
export type TwigCompletionOption = (
|
||||
| TwigCompletionOptionFunction
|
||||
| TwigCompletionOptionVariable
|
||||
| TwigCompletionOptionNamespace
|
||||
) & {
|
||||
name: string;
|
||||
label: string;
|
||||
onClick: (rawTag: string, startPos: number) => void;
|
||||
@@ -25,12 +33,12 @@ export interface TwigCompletionConfig {
|
||||
}
|
||||
|
||||
const MIN_MATCH_VAR = 1;
|
||||
const MIN_MATCH_NAME = 2;
|
||||
const MIN_MATCH_NAME = 1;
|
||||
|
||||
export function twigCompletion({ options }: TwigCompletionConfig) {
|
||||
return function completions(context: CompletionContext) {
|
||||
const toStartOfName = context.matchBefore(/\w*/);
|
||||
const toStartOfVariable = context.matchBefore(/\$\{?\[?\s*\w*/);
|
||||
const toStartOfName = context.matchBefore(/[\w_.]*/);
|
||||
const toStartOfVariable = context.matchBefore(/\$\{?\[?\s*[\w_]*/);
|
||||
const toMatch = toStartOfVariable ?? toStartOfName ?? null;
|
||||
|
||||
if (toMatch === null) return null;
|
||||
@@ -47,22 +55,37 @@ export function twigCompletion({ options }: TwigCompletionConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const completions: Completion[] = options
|
||||
.map((o): Completion => {
|
||||
const matchSegments = toStartOfName!.text.split('.');
|
||||
const optionSegments = o.name.split('.');
|
||||
|
||||
// If not on the last segment, only complete the namespace
|
||||
if (matchSegments.length < optionSegments.length) {
|
||||
return {
|
||||
label: optionSegments.slice(0, matchSegments.length).join('.'),
|
||||
apply: optionSegments.slice(0, matchSegments.length).join('.'),
|
||||
type: 'namespace',
|
||||
};
|
||||
}
|
||||
|
||||
// If on the last segment, wrap the entire tag
|
||||
const inner = o.type === 'function' ? `${o.name}()` : o.name;
|
||||
return {
|
||||
label: o.name,
|
||||
apply: openTag + inner + closeTag,
|
||||
type: o.type === 'variable' ? 'variable' : 'function',
|
||||
};
|
||||
})
|
||||
.filter((v) => v != null);
|
||||
|
||||
// TODO: Figure out how to make autocomplete stay open if opened explicitly. It sucks when you explicitly
|
||||
// open it, then it closes when you type the next character.
|
||||
return {
|
||||
validFor: () => true, // Not really sure why this is all it needs
|
||||
from: toMatch.from,
|
||||
options: options
|
||||
.filter((v) => v.name.trim())
|
||||
.map((v) => {
|
||||
const inner = v.type === 'function' ? `${v.name}()` : v.name;
|
||||
return {
|
||||
label: v.label,
|
||||
apply: openTag + inner + closeTag,
|
||||
type: v.type === 'variable' ? 'variable' : 'function',
|
||||
matchLen: matchLen,
|
||||
};
|
||||
})
|
||||
matchLen,
|
||||
options: completions
|
||||
// Filter out exact matches
|
||||
.filter((o) => o.label !== toMatch.text),
|
||||
};
|
||||
|
||||
@@ -9,7 +9,11 @@ import type { TwigCompletionOption } from './completion';
|
||||
class PathPlaceholderWidget extends WidgetType {
|
||||
readonly #clickListenerCallback: () => void;
|
||||
|
||||
constructor(readonly rawText: string, readonly startPos: number, readonly onClick: () => void) {
|
||||
constructor(
|
||||
readonly rawText: string,
|
||||
readonly startPos: number,
|
||||
readonly onClick: () => void,
|
||||
) {
|
||||
super();
|
||||
this.#clickListenerCallback = () => {
|
||||
this.onClick?.();
|
||||
@@ -68,15 +72,15 @@ class TemplateTagWidget extends WidgetType {
|
||||
this.option.invalid
|
||||
? 'x-theme-templateTag--danger'
|
||||
: this.option.type === 'variable'
|
||||
? 'x-theme-templateTag--primary'
|
||||
: 'x-theme-templateTag--info'
|
||||
? 'x-theme-templateTag--primary'
|
||||
: 'x-theme-templateTag--info'
|
||||
}`;
|
||||
elt.title = this.option.invalid ? 'Not Found' : this.option.value ?? '';
|
||||
elt.title = this.option.invalid ? 'Not Found' : (this.option.value ?? '');
|
||||
elt.setAttribute('data-tag-type', this.option.type);
|
||||
elt.textContent =
|
||||
this.option.type === 'variable'
|
||||
? this.option.name
|
||||
: `${this.option.name}(${this.option.args.length ? '…' : ''})`;
|
||||
this.option.type === 'function'
|
||||
? `${this.option.name}(${this.option.args.length ? '…' : ''})`
|
||||
: this.option.name;
|
||||
elt.addEventListener('click', this.#clickListenerCallback);
|
||||
return elt;
|
||||
}
|
||||
@@ -134,7 +138,7 @@ function templateTags(
|
||||
|
||||
// TODO: Search `node.tree` instead of using Regex here
|
||||
const inner = rawTag.replace(/^\$\{\[\s*/, '').replace(/\s*]}$/, '');
|
||||
let name = inner.match(/(\w+)[(]/)?.[1] ?? inner;
|
||||
let name = inner.match(/([\w.]+)[(]/)?.[1] ?? inner;
|
||||
|
||||
// The beta named the function `Response` but was changed in stable.
|
||||
// Keep this here for a while because there's no easy way to migrate
|
||||
|
||||
@@ -56,7 +56,7 @@ export const VStack = forwardRef(function VStack(
|
||||
});
|
||||
|
||||
type BaseStackProps = HTMLAttributes<HTMLElement> & {
|
||||
as?: ComponentType | 'ul' | 'label' | 'form';
|
||||
as?: ComponentType | 'ul' | 'label' | 'form' | 'p';
|
||||
space?: keyof typeof gapClasses;
|
||||
alignItems?: 'start' | 'center' | 'stretch' | 'end';
|
||||
justifyContent?: 'start' | 'center' | 'end' | 'between';
|
||||
|
||||
@@ -5,12 +5,13 @@ import type {
|
||||
HttpRequest,
|
||||
} from '@yaakapp/api';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { usePlugins } from './usePlugins';
|
||||
import { usePluginsKey } from './usePlugins';
|
||||
|
||||
export function useHttpRequestActions() {
|
||||
const plugins = usePlugins();
|
||||
const pluginsKey = usePluginsKey();
|
||||
|
||||
const httpRequestActions = useQuery({
|
||||
queryKey: ['http_request_actions', plugins.map((p) => p.updatedAt)],
|
||||
queryKey: ['http_request_actions', pluginsKey],
|
||||
refetchOnWindowFocus: false,
|
||||
queryFn: async () => {
|
||||
const responses = (await invokeCmd(
|
||||
|
||||
@@ -2,11 +2,11 @@ import { useMutation } from '@tanstack/react-query';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
|
||||
export function useCreatePlugin() {
|
||||
export function useInstallPlugin() {
|
||||
return useMutation<void, unknown, string>({
|
||||
mutationKey: ['create_plugin'],
|
||||
mutationKey: ['install_plugin'],
|
||||
mutationFn: async (directory: string) => {
|
||||
await invokeCmd('cmd_create_plugin', { directory });
|
||||
await invokeCmd('cmd_install_plugin', { directory });
|
||||
},
|
||||
onSettled: () => trackEvent('plugin', 'create'),
|
||||
});
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { PluginBootResponse } from '@yaakapp/api';
|
||||
import type { BootResponse } from '@yaakapp/api';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
|
||||
export function usePluginInfo(id: string) {
|
||||
return useQuery({
|
||||
queryKey: ['plugin_info', id],
|
||||
queryFn: async () => {
|
||||
const info = (await invokeCmd('cmd_plugin_info', { id })) as PluginBootResponse;
|
||||
const info = (await invokeCmd('cmd_plugin_info', { id })) as BootResponse;
|
||||
return info;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -12,6 +12,12 @@ export function usePlugins() {
|
||||
return useAtomValue(pluginsAtom);
|
||||
}
|
||||
|
||||
export function usePluginsKey() {
|
||||
return useAtomValue(pluginsAtom)
|
||||
.map((p) => p.id + p.updatedAt)
|
||||
.join(',');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload all plugins and refresh the list of plugins
|
||||
*/
|
||||
|
||||
@@ -8,6 +8,7 @@ export function useRenderTemplate(template: string) {
|
||||
const environmentId = useActiveEnvironment()[0]?.id ?? null;
|
||||
return useQuery<string>({
|
||||
placeholderData: (prev) => prev, // Keep previous data on refetch
|
||||
refetchOnWindowFocus: false,
|
||||
queryKey: ['render_template', template],
|
||||
queryFn: () => renderTemplate({ template, workspaceId, environmentId }),
|
||||
});
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { GetTemplateFunctionsResponse } from '@yaakapp/api';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
import { usePluginsKey } from './usePlugins';
|
||||
|
||||
export function useTemplateFunctions() {
|
||||
const pluginsKey = usePluginsKey();
|
||||
|
||||
const result = useQuery({
|
||||
queryKey: ['template_functions'],
|
||||
refetchOnWindowFocus: false,
|
||||
queryKey: ['template_functions', pluginsKey],
|
||||
queryFn: async () => {
|
||||
const responses = (await invokeCmd(
|
||||
'cmd_template_functions',
|
||||
@@ -14,6 +16,5 @@ export function useTemplateFunctions() {
|
||||
},
|
||||
});
|
||||
|
||||
const fns = result.data?.flatMap((r) => r.functions) ?? [];
|
||||
return fns;
|
||||
return result.data?.flatMap((r) => r.functions) ?? [];
|
||||
}
|
||||
|
||||
14
src-web/hooks/useUninstallPlugin.ts
Normal file
14
src-web/hooks/useUninstallPlugin.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { Plugin } from '@yaakapp/api';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { invokeCmd } from '../lib/tauri';
|
||||
|
||||
export function useUninstallPlugin(pluginId: string) {
|
||||
return useMutation<Plugin | null, string>({
|
||||
mutationKey: ['uninstall_plugin'],
|
||||
mutationFn: async () => {
|
||||
return invokeCmd('cmd_uninstall_plugin', { pluginId });
|
||||
},
|
||||
onSettled: () => trackEvent('plugin', 'delete'),
|
||||
});
|
||||
}
|
||||
@@ -6,7 +6,6 @@ type TauriCmd =
|
||||
| 'cmd_check_for_updates'
|
||||
| 'cmd_create_cookie_jar'
|
||||
| 'cmd_create_environment'
|
||||
| 'cmd_create_plugin'
|
||||
| 'cmd_template_tokens_to_string'
|
||||
| 'cmd_create_folder'
|
||||
| 'cmd_create_grpc_request'
|
||||
@@ -40,6 +39,7 @@ type TauriCmd =
|
||||
| 'cmd_grpc_reflect'
|
||||
| 'cmd_http_request_actions'
|
||||
| 'cmd_import_data'
|
||||
| 'cmd_install_plugin'
|
||||
| 'cmd_list_cookie_jars'
|
||||
| 'cmd_list_environments'
|
||||
| 'cmd_list_folders'
|
||||
@@ -64,6 +64,7 @@ type TauriCmd =
|
||||
| 'cmd_set_update_mode'
|
||||
| 'cmd_template_functions'
|
||||
| 'cmd_track_event'
|
||||
| 'cmd_uninstall_plugin'
|
||||
| 'cmd_update_cookie_jar'
|
||||
| 'cmd_update_environment'
|
||||
| 'cmd_update_folder'
|
||||
|
||||
@@ -19,10 +19,18 @@
|
||||
}
|
||||
|
||||
/* Disable user selection to make it more "app-like" */
|
||||
:not(a),
|
||||
:not(input):not(textarea),
|
||||
:not(input):not(textarea)::after,
|
||||
:not(input):not(textarea)::before {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p,
|
||||
blockquote,
|
||||
label,
|
||||
code,
|
||||
pre,
|
||||
li {
|
||||
@apply select-none cursor-default;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user