commit c3e5b84282af4f7a171851b14809f579810d1238 Author: Jonas Kruckenberg Date: Tue Nov 1 15:19:25 2022 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f7f5807 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +node_modules +src/*.mjs \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..4044827 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tauri"] + path = tauri + url = https://github.com/tauri-apps/tauri diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e598402 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,432 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "async-stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad5c83079eae9969be7fadefe640a1c566901f05ff91ab221de4b6f68d9507e" +dependencies = [ + "async-stream-impl", + "futures-core", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bumpalo" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" + +[[package]] +name = "bytes" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "itoa" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" + +[[package]] +name = "js-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "once_cell" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "proc-macro2" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "semver" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" + +[[package]] +name = "serde" +version = "1.0.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b4c031cd0d9014307d82b8abf653c0290fbdaeb4c02d00c63cf52f728628bf" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_derive" +version = "1.0.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tauri-sys" +version = "0.1.0" +dependencies = [ + "js-sys", + "semver", + "serde", + "serde-wasm-bindgen", + "thiserror", + "tokio-test", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", +] + +[[package]] +name = "thiserror" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" +dependencies = [ + "autocfg", + "pin-project-lite", +] + +[[package]] +name = "tokio-stream" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53474327ae5e166530d17f2d956afcb4f8a004de581b3cae10f12006bc8163e3" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-ident" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +dependencies = [ + "cfg-if", + "serde_json", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d2fff962180c3fadf677438054b1db62bee4aa32af26a45388af07d1287e1d" +dependencies = [ + "console_error_panic_hook", + "js-sys", + "scoped-tls", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4683da3dfc016f704c9f82cf401520c4f1cb3ee440f7f52b3d6ac29506a49ca7" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "web-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5661dfb --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "tauri-sys" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde-wasm-bindgen = "0.4.3" +js-sys = "0.3.59" +serde = { version = "1.0.140", features = ["derive"] } +wasm-bindgen = { version = "0.2.82", features = ["serde_json"] } +wasm-bindgen-futures = "0.4.32" +url = { version = "2.3.1", optional = true } +thiserror = "1.0.37" +semver = { version = "1.0.14", optional = true } + +[dev-dependencies] +tokio-test = "0.4.2" +wasm-bindgen-test = "0.3.33" + +[features] +default = ["all"] +all = ["app", "clipboard", "event", "mocks", "tauri"] +app = ["dep:semver"] +clipboard = [] +event = [] +mocks = [] +tauri = ["dep:url"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a84e0b9 --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# tauri-sys + +Bindings to the [Tauri API] for projects using [wasm-bindgen] + +## Installation + +This crate is not yet published to crates.io, so you need to use it from git. +You also need a global installation of [`esbuild`]. + +```toml +tauri-sys = { git = "https://github.com/JonasKruckenberg/tauri-sys" } +``` + +## Usage + +```rust +use serde::{Deserialize, Serialize}; +use tauri_sys::tauri; + +#[derive(Serialize, Deserialize)] +struct GreetArgs<'a> { + name: &'a str, +} + +fn main() { + wasm_bindgen_futures::spawn_local(async move { + let new_msg: String = tauri::invoke("greet", &GreetArgs { name: &name.get() }).await.unwrap(); + + println!("{}", new_msg); + }); +} +``` + +## Features + +All modules are gated by accordingly named Cargo features. It is recommended you keep this synced with the features enabled in your [Tauri Allowlist] but no automated tool for this exists (yet). + +- **all**: Enables all modules. +- **app**: Enables the `app` module. +- **clipboard**: Enables the `clipboard` module. +- **event**: Enables the `event` module. +- **mocks**: Enables the `mocks` module. +- **tauri**: Enables the `tauri` module. + +## Are we Tauri yet? + +These API bindings are not completely on-par with `@tauri-apps/api` yet, but here is the current status-quo: + +- [x] `app` +- [ ] `cli` +- [x] `clipboard` +- [ ] `dialog` +- [x] `event` +- [ ] `fs` +- [ ] `global_shortcut` +- [ ] `http` +- [x] `mocks` +- [ ] `notification` +- [ ] `os` +- [ ] `path` +- [ ] `process` +- [ ] `shell` +- [x] `tauri` +- [ ] `updater` +- [ ] `window` + +The current API also very closely mirrors the JS API even though that might not be the most ergonomic choice, ideas for improving the API with quality-of-life features beyond the regular JS API interface are very welcome. + +[Tauri API]: https://tauri.app/v1/api/js/ +[wasm-bindgen]: https://github.com/rustwasm/wasm-bindgen +[Tauri Allowlist]: https://tauri.app/v1/api/config#allowlistconfig +[`esbuild`]: https://esbuild.github.io/getting-started/#install-esbuild \ No newline at end of file diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..388ce1f --- /dev/null +++ b/build.rs @@ -0,0 +1,17 @@ +use std::process::Command; + +fn main() { + Command::new("esbuild") + .args([ + "--outdir=dist", + "--format=esm", + "--bundle", + "tauri/tooling/api/src/app.ts", + "tauri/tooling/api/src/clipboard.ts", + "tauri/tooling/api/src/tauri.ts", + "tauri/tooling/api/src/event.ts", + "tauri/tooling/api/src/mocks.ts" + ]) + .output() + .unwrap(); +} diff --git a/dist/app.js b/dist/app.js new file mode 100644 index 0000000..6b66a23 --- /dev/null +++ b/dist/app.js @@ -0,0 +1,91 @@ +// tauri/tooling/api/src/tauri.ts +function uid() { + return window.crypto.getRandomValues(new Uint32Array(1))[0]; +} +function transformCallback(callback, once = false) { + const identifier = uid(); + const prop = `_${identifier}`; + Object.defineProperty(window, prop, { + value: (result) => { + if (once) { + Reflect.deleteProperty(window, prop); + } + return callback?.(result); + }, + writable: false, + configurable: true + }); + return identifier; +} +async function invoke(cmd, args = {}) { + return new Promise((resolve, reject) => { + const callback = transformCallback((e) => { + resolve(e); + Reflect.deleteProperty(window, `_${error}`); + }, true); + const error = transformCallback((e) => { + reject(e); + Reflect.deleteProperty(window, `_${callback}`); + }, true); + window.__TAURI_IPC__({ + cmd, + callback, + error, + ...args + }); + }); +} + +// tauri/tooling/api/src/helpers/tauri.ts +async function invokeTauriCommand(command) { + return invoke("tauri", command); +} + +// tauri/tooling/api/src/app.ts +async function getVersion() { + return invokeTauriCommand({ + __tauriModule: "App", + message: { + cmd: "getAppVersion" + } + }); +} +async function getName() { + return invokeTauriCommand({ + __tauriModule: "App", + message: { + cmd: "getAppName" + } + }); +} +async function getTauriVersion() { + return invokeTauriCommand({ + __tauriModule: "App", + message: { + cmd: "getTauriVersion" + } + }); +} +async function show() { + return invokeTauriCommand({ + __tauriModule: "App", + message: { + cmd: "show" + } + }); +} +async function hide() { + return invokeTauriCommand({ + __tauriModule: "App", + message: { + cmd: "hide" + } + }); +} +export { + getName, + getTauriVersion, + getVersion, + hide, + show +}; diff --git a/dist/clipboard.js b/dist/clipboard.js new file mode 100644 index 0000000..16bd040 --- /dev/null +++ b/dist/clipboard.js @@ -0,0 +1,66 @@ +// tauri/tooling/api/src/tauri.ts +function uid() { + return window.crypto.getRandomValues(new Uint32Array(1))[0]; +} +function transformCallback(callback, once = false) { + const identifier = uid(); + const prop = `_${identifier}`; + Object.defineProperty(window, prop, { + value: (result) => { + if (once) { + Reflect.deleteProperty(window, prop); + } + return callback?.(result); + }, + writable: false, + configurable: true + }); + return identifier; +} +async function invoke(cmd, args = {}) { + return new Promise((resolve, reject) => { + const callback = transformCallback((e) => { + resolve(e); + Reflect.deleteProperty(window, `_${error}`); + }, true); + const error = transformCallback((e) => { + reject(e); + Reflect.deleteProperty(window, `_${callback}`); + }, true); + window.__TAURI_IPC__({ + cmd, + callback, + error, + ...args + }); + }); +} + +// tauri/tooling/api/src/helpers/tauri.ts +async function invokeTauriCommand(command) { + return invoke("tauri", command); +} + +// tauri/tooling/api/src/clipboard.ts +async function writeText(text) { + return invokeTauriCommand({ + __tauriModule: "Clipboard", + message: { + cmd: "writeText", + data: text + } + }); +} +async function readText() { + return invokeTauriCommand({ + __tauriModule: "Clipboard", + message: { + cmd: "readText", + data: null + } + }); +} +export { + readText, + writeText +}; diff --git a/dist/event.js b/dist/event.js new file mode 100644 index 0000000..934f20b --- /dev/null +++ b/dist/event.js @@ -0,0 +1,123 @@ +// tauri/tooling/api/src/tauri.ts +function uid() { + return window.crypto.getRandomValues(new Uint32Array(1))[0]; +} +function transformCallback(callback, once3 = false) { + const identifier = uid(); + const prop = `_${identifier}`; + Object.defineProperty(window, prop, { + value: (result) => { + if (once3) { + Reflect.deleteProperty(window, prop); + } + return callback?.(result); + }, + writable: false, + configurable: true + }); + return identifier; +} +async function invoke(cmd, args = {}) { + return new Promise((resolve, reject) => { + const callback = transformCallback((e) => { + resolve(e); + Reflect.deleteProperty(window, `_${error}`); + }, true); + const error = transformCallback((e) => { + reject(e); + Reflect.deleteProperty(window, `_${callback}`); + }, true); + window.__TAURI_IPC__({ + cmd, + callback, + error, + ...args + }); + }); +} + +// tauri/tooling/api/src/helpers/tauri.ts +async function invokeTauriCommand(command) { + return invoke("tauri", command); +} + +// tauri/tooling/api/src/helpers/event.ts +async function _unlisten(event, eventId) { + return invokeTauriCommand({ + __tauriModule: "Event", + message: { + cmd: "unlisten", + event, + eventId + } + }); +} +async function emit(event, windowLabel, payload) { + await invokeTauriCommand({ + __tauriModule: "Event", + message: { + cmd: "emit", + event, + windowLabel, + payload + } + }); +} +async function listen(event, windowLabel, handler) { + return invokeTauriCommand({ + __tauriModule: "Event", + message: { + cmd: "listen", + event, + windowLabel, + handler: transformCallback(handler) + } + }).then((eventId) => { + return async () => _unlisten(event, eventId); + }); +} +async function once(event, windowLabel, handler) { + return listen(event, windowLabel, (eventData) => { + handler(eventData); + _unlisten(event, eventData.id).catch(() => { + }); + }); +} + +// tauri/tooling/api/src/event.ts +var TauriEvent = /* @__PURE__ */ ((TauriEvent2) => { + TauriEvent2["WINDOW_RESIZED"] = "tauri://resize"; + TauriEvent2["WINDOW_MOVED"] = "tauri://move"; + TauriEvent2["WINDOW_CLOSE_REQUESTED"] = "tauri://close-requested"; + TauriEvent2["WINDOW_CREATED"] = "tauri://window-created"; + TauriEvent2["WINDOW_DESTROYED"] = "tauri://destroyed"; + TauriEvent2["WINDOW_FOCUS"] = "tauri://focus"; + TauriEvent2["WINDOW_BLUR"] = "tauri://blur"; + TauriEvent2["WINDOW_SCALE_FACTOR_CHANGED"] = "tauri://scale-change"; + TauriEvent2["WINDOW_THEME_CHANGED"] = "tauri://theme-changed"; + TauriEvent2["WINDOW_FILE_DROP"] = "tauri://file-drop"; + TauriEvent2["WINDOW_FILE_DROP_HOVER"] = "tauri://file-drop-hover"; + TauriEvent2["WINDOW_FILE_DROP_CANCELLED"] = "tauri://file-drop-cancelled"; + TauriEvent2["MENU"] = "tauri://menu"; + TauriEvent2["CHECK_UPDATE"] = "tauri://update"; + TauriEvent2["UPDATE_AVAILABLE"] = "tauri://update-available"; + TauriEvent2["INSTALL_UPDATE"] = "tauri://update-install"; + TauriEvent2["STATUS_UPDATE"] = "tauri://update-status"; + TauriEvent2["DOWNLOAD_PROGRESS"] = "tauri://update-download-progress"; + return TauriEvent2; +})(TauriEvent || {}); +async function listen2(event, handler) { + return listen(event, null, handler); +} +async function once2(event, handler) { + return once(event, null, handler); +} +async function emit2(event, payload) { + return emit(event, void 0, payload); +} +export { + TauriEvent, + emit2 as emit, + listen2 as listen, + once2 as once +}; diff --git a/dist/mocks.js b/dist/mocks.js new file mode 100644 index 0000000..4342d8f --- /dev/null +++ b/dist/mocks.js @@ -0,0 +1,30 @@ +// tauri/tooling/api/src/mocks.ts +function mockIPC(cb) { + window.__TAURI_IPC__ = async ({ + cmd, + callback, + error, + ...args + }) => { + try { + window[`_${callback}`](await cb(cmd, args)); + } catch (err) { + window[`_${error}`](err); + } + }; +} +function mockWindows(current, ...additionalWindows) { + window.__TAURI_METADATA__ = { + __windows: [current, ...additionalWindows].map((label) => ({ label })), + __currentWindow: { label: current } + }; +} +function clearMocks() { + delete window.__TAURI_IPC__; + delete window.__TAURI_METADATA__; +} +export { + clearMocks, + mockIPC, + mockWindows +}; diff --git a/dist/tauri.js b/dist/tauri.js new file mode 100644 index 0000000..5360e6f --- /dev/null +++ b/dist/tauri.js @@ -0,0 +1,46 @@ +// tauri/tooling/api/src/tauri.ts +function uid() { + return window.crypto.getRandomValues(new Uint32Array(1))[0]; +} +function transformCallback(callback, once = false) { + const identifier = uid(); + const prop = `_${identifier}`; + Object.defineProperty(window, prop, { + value: (result) => { + if (once) { + Reflect.deleteProperty(window, prop); + } + return callback?.(result); + }, + writable: false, + configurable: true + }); + return identifier; +} +async function invoke(cmd, args = {}) { + return new Promise((resolve, reject) => { + const callback = transformCallback((e) => { + resolve(e); + Reflect.deleteProperty(window, `_${error}`); + }, true); + const error = transformCallback((e) => { + reject(e); + Reflect.deleteProperty(window, `_${callback}`); + }, true); + window.__TAURI_IPC__({ + cmd, + callback, + error, + ...args + }); + }); +} +function convertFileSrc(filePath, protocol = "asset") { + const path = encodeURIComponent(filePath); + return navigator.userAgent.includes("Windows") ? `https://${protocol}.localhost/${path}` : `${protocol}://localhost/${path}`; +} +export { + convertFileSrc, + invoke, + transformCallback +}; diff --git a/examples/api/.gitignore b/examples/api/.gitignore new file mode 100644 index 0000000..48c3ca4 --- /dev/null +++ b/examples/api/.gitignore @@ -0,0 +1,3 @@ +/dist/ +/target/ +/Cargo.lock diff --git a/examples/api/.taurignore b/examples/api/.taurignore new file mode 100644 index 0000000..1ebdc6d --- /dev/null +++ b/examples/api/.taurignore @@ -0,0 +1,3 @@ +/src +/public +/Cargo.toml \ No newline at end of file diff --git a/examples/api/Cargo.toml b/examples/api/Cargo.toml new file mode 100644 index 0000000..3ce3c74 --- /dev/null +++ b/examples/api/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "tauri-app-ui" +version = "0.0.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[dependencies] +tauri-sys = { path = "../../" } +serde = { version = "1.0.140", features = ["derive"] } +sycamore = { git = "https://github.com/sycamore-rs/sycamore", rev = "abd556cbc02047042dad2ebd04405e455a9b11b2", features = ["suspense", "hydrate"] } +sycamore-router = { git = "https://github.com/sycamore-rs/sycamore", rev = "abd556cbc02047042dad2ebd04405e455a9b11b2" } +log = "0.4.17" +wasm-logger = "0.2.0" +gloo-timers = "0.2.4" +shared = { path = "shared" } + +[features] +ssg = ["sycamore/ssr"] + +[workspace] +members = ["src-tauri", "shared"] diff --git a/examples/api/Trunk.toml b/examples/api/Trunk.toml new file mode 100644 index 0000000..b66ce3e --- /dev/null +++ b/examples/api/Trunk.toml @@ -0,0 +1,16 @@ +[build] +target = "./index.html" + +[watch] +ignore = ["./src-tauri"] + +[serve] +address = "127.0.0.1" +port = 1420 +open = false + +[[hooks]] +# Runs SSG on production builds +stage = "post_build" +command = "bash" +command_arguments = ["-c", "if [[ $TRUNK_PROFILE == \"release\" ]]; then cargo run --release --features ssg -- $TRUNK_STAGING_DIR; fi"] diff --git a/examples/api/index.html b/examples/api/index.html new file mode 100644 index 0000000..fc029e0 --- /dev/null +++ b/examples/api/index.html @@ -0,0 +1,12 @@ + + + + + Tauri + Yew App + + + + + + + diff --git a/examples/api/public/tauri_logo.png b/examples/api/public/tauri_logo.png new file mode 100644 index 0000000..2c53b8c Binary files /dev/null and b/examples/api/public/tauri_logo.png differ diff --git a/examples/api/shared/Cargo.toml b/examples/api/shared/Cargo.toml new file mode 100644 index 0000000..92a9d17 --- /dev/null +++ b/examples/api/shared/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "shared" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1.0.140", features = ["derive"] } \ No newline at end of file diff --git a/examples/api/shared/src/lib.rs b/examples/api/shared/src/lib.rs new file mode 100644 index 0000000..1ebbc1d --- /dev/null +++ b/examples/api/shared/src/lib.rs @@ -0,0 +1,12 @@ +use serde::{Serialize, Deserialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Reply<'a> { + pub data: &'a str, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RequestBody<'a> { + pub id: i32, + pub name: &'a str, +} \ No newline at end of file diff --git a/examples/api/src-tauri/.gitignore b/examples/api/src-tauri/.gitignore new file mode 100644 index 0000000..f4dfb82 --- /dev/null +++ b/examples/api/src-tauri/.gitignore @@ -0,0 +1,4 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + diff --git a/examples/api/src-tauri/Cargo.toml b/examples/api/src-tauri/Cargo.toml new file mode 100644 index 0000000..ee6ef6f --- /dev/null +++ b/examples/api/src-tauri/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "tauri-app" +version = "0.0.0" +description = "A Tauri App" +authors = ["you"] +license = "" +repository = "" +edition = "2021" +rust-version = "1.57" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[build-dependencies] +tauri-build = { git = "https://github.com/tauri-apps/tauri", features = [] } + +[dependencies] +serde_json = "1.0" +serde = { version = "1.0", features = ["derive"] } +tauri = { git = "https://github.com/tauri-apps/tauri", features = ["api-all"] } +shared = { path = "../shared" } + +[features] +# by default Tauri runs in production mode +# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL +default = [ "custom-protocol" ] +# this feature is used used for production builds where `devPath` points to the filesystem +# DO NOT remove this +custom-protocol = [ "tauri/custom-protocol" ] diff --git a/examples/api/src-tauri/build.rs b/examples/api/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/examples/api/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/examples/api/src-tauri/icons/128x128.png b/examples/api/src-tauri/icons/128x128.png new file mode 100644 index 0000000..6be5e50 Binary files /dev/null and b/examples/api/src-tauri/icons/128x128.png differ diff --git a/examples/api/src-tauri/icons/128x128@2x.png b/examples/api/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000..e81bece Binary files /dev/null and b/examples/api/src-tauri/icons/128x128@2x.png differ diff --git a/examples/api/src-tauri/icons/32x32.png b/examples/api/src-tauri/icons/32x32.png new file mode 100644 index 0000000..a437dd5 Binary files /dev/null and b/examples/api/src-tauri/icons/32x32.png differ diff --git a/examples/api/src-tauri/icons/Square107x107Logo.png b/examples/api/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000..0ca4f27 Binary files /dev/null and b/examples/api/src-tauri/icons/Square107x107Logo.png differ diff --git a/examples/api/src-tauri/icons/Square142x142Logo.png b/examples/api/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000..b81f820 Binary files /dev/null and b/examples/api/src-tauri/icons/Square142x142Logo.png differ diff --git a/examples/api/src-tauri/icons/Square150x150Logo.png b/examples/api/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000..624c7bf Binary files /dev/null and b/examples/api/src-tauri/icons/Square150x150Logo.png differ diff --git a/examples/api/src-tauri/icons/Square284x284Logo.png b/examples/api/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000..c021d2b Binary files /dev/null and b/examples/api/src-tauri/icons/Square284x284Logo.png differ diff --git a/examples/api/src-tauri/icons/Square30x30Logo.png b/examples/api/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000..6219700 Binary files /dev/null and b/examples/api/src-tauri/icons/Square30x30Logo.png differ diff --git a/examples/api/src-tauri/icons/Square310x310Logo.png b/examples/api/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000..f9bc048 Binary files /dev/null and b/examples/api/src-tauri/icons/Square310x310Logo.png differ diff --git a/examples/api/src-tauri/icons/Square44x44Logo.png b/examples/api/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000..d5fbfb2 Binary files /dev/null and b/examples/api/src-tauri/icons/Square44x44Logo.png differ diff --git a/examples/api/src-tauri/icons/Square71x71Logo.png b/examples/api/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000..63440d7 Binary files /dev/null and b/examples/api/src-tauri/icons/Square71x71Logo.png differ diff --git a/examples/api/src-tauri/icons/Square89x89Logo.png b/examples/api/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000..f3f705a Binary files /dev/null and b/examples/api/src-tauri/icons/Square89x89Logo.png differ diff --git a/examples/api/src-tauri/icons/StoreLogo.png b/examples/api/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000..4556388 Binary files /dev/null and b/examples/api/src-tauri/icons/StoreLogo.png differ diff --git a/examples/api/src-tauri/icons/icon.icns b/examples/api/src-tauri/icons/icon.icns new file mode 100644 index 0000000..12a5bce Binary files /dev/null and b/examples/api/src-tauri/icons/icon.icns differ diff --git a/examples/api/src-tauri/icons/icon.ico b/examples/api/src-tauri/icons/icon.ico new file mode 100644 index 0000000..b3636e4 Binary files /dev/null and b/examples/api/src-tauri/icons/icon.ico differ diff --git a/examples/api/src-tauri/icons/icon.png b/examples/api/src-tauri/icons/icon.png new file mode 100644 index 0000000..e1cd261 Binary files /dev/null and b/examples/api/src-tauri/icons/icon.png differ diff --git a/examples/api/src-tauri/src/main.rs b/examples/api/src-tauri/src/main.rs new file mode 100644 index 0000000..84d72bd --- /dev/null +++ b/examples/api/src-tauri/src/main.rs @@ -0,0 +1,38 @@ +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] + +use serde::Deserialize; +use shared::{Reply, RequestBody}; + +#[tauri::command] +fn log_operation(event: String, payload: Option) { + println!("{} {:?}", event, payload); +} + +#[tauri::command] +fn perform_request(endpoint: String, body: RequestBody) -> String { + println!("{} {:?}", endpoint, body); + "message response".into() +} + +fn main() { + tauri::Builder::default() + .invoke_handler(tauri::generate_handler![log_operation, perform_request]) + .on_page_load(|window, _| { + let window_ = window.clone(); + window.listen("js-event", move |event| { + println!("got js-event with message '{:?}'", event.payload()); + let reply = Reply { + data: "something else", + }; + + window_ + .emit("rust-event", Some(reply)) + .expect("failed to emit"); + }); + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/examples/api/src-tauri/tauri.conf.json b/examples/api/src-tauri/tauri.conf.json new file mode 100644 index 0000000..15e818e --- /dev/null +++ b/examples/api/src-tauri/tauri.conf.json @@ -0,0 +1,66 @@ +{ + "build": { + "beforeDevCommand": "trunk serve", + "beforeBuildCommand": "trunk build --release", + "devPath": "http://localhost:1420", + "distDir": "../dist", + "withGlobalTauri": true + }, + "package": { + "productName": "tauri-app", + "version": "0.0.0" + }, + "tauri": { + "allowlist": { + "all": true + }, + "bundle": { + "active": true, + "category": "DeveloperTool", + "copyright": "", + "deb": { + "depends": [] + }, + "externalBin": [], + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "identifier": "com.tauri.dev", + "longDescription": "", + "macOS": { + "entitlements": null, + "exceptionDomain": "", + "frameworks": [], + "providerShortName": null, + "signingIdentity": null + }, + "resources": [], + "shortDescription": "", + "targets": "all", + "windows": { + "certificateThumbprint": null, + "digestAlgorithm": "sha256", + "timestampUrl": "" + } + }, + "security": { + "csp": null + }, + "updater": { + "active": false + }, + "windows": [ + { + "fullscreen": false, + "height": 600, + "resizable": true, + "title": "tauri-app", + "width": 800 + } + ] + } +} diff --git a/examples/api/src/main.rs b/examples/api/src/main.rs new file mode 100644 index 0000000..c317bcc --- /dev/null +++ b/examples/api/src/main.rs @@ -0,0 +1,79 @@ +mod views; + +use sycamore::prelude::*; +#[cfg(not(feature = "ssg"))] +use sycamore_router::{Router, HistoryIntegration}; + +#[component] +fn Header(cx: Scope) -> View { + view! { cx, + header(style="display: flex; gap: 1em; margin-bottom: 1em;") { + a(href="/") { + "Welcome" + } + a(href="/app") { + "App" + } + a(href="/clipboard") { + "Clipboard" + } + a(href="/communication") { + "Communication" + } + } + } +} + +#[cfg(all(not(debug_assertions), not(feature = "ssg")))] +fn main() { + wasm_logger::init(wasm_logger::Config::default()); + + sycamore::hydrate(|cx| view! { cx, + Header + Router( + integration=HistoryIntegration::new(), + view=views::switch + ) + }); +} + +#[cfg(all(debug_assertions, not(feature = "ssg")))] +fn main() { + use sycamore::view; + + wasm_logger::init(wasm_logger::Config::default()); + + sycamore::render(|cx| view! { cx, + Header + Router( + integration=HistoryIntegration::new(), + view=views::switch + ) + }); +} + +#[cfg(feature = "ssg")] +fn main() { + use sycamore_router::StaticRouter; + + let out_dir = std::env::args().nth(1).unwrap(); + + println!("out_dir {}", out_dir); + + let template = std::fs::read_to_string(format!("{}/index.html", out_dir)).unwrap(); + + let html = sycamore::render_to_string(|cx| view! { cx, + Header + StaticRouter( + route=route.clone(), + view=views::switch + ) + }); + + let html = template.replace("\n", &html); + + let path = format!("{}/index.html", out_dir); + + println!("Writing html to file \"{}\"", path); + std::fs::write(path, html).unwrap(); +} diff --git a/examples/api/src/views/app.rs b/examples/api/src/views/app.rs new file mode 100644 index 0000000..3127131 --- /dev/null +++ b/examples/api/src/views/app.rs @@ -0,0 +1,78 @@ +use gloo_timers::callback::Timeout; +use sycamore::prelude::*; +use tauri_api::app; + +#[component] +pub fn App(cx: Scope) -> View { + let show_app = |_| { + sycamore::futures::spawn_local(async move { + let res = app::hide().await; + + log::debug!("app hide res {:?}", res); + + let timeout = Timeout::new(2_000, move || { + sycamore::futures::spawn_local(async move { + let res = app::show().await; + + log::debug!("app show res {:?}", res); + }); + }); + + timeout.forget(); + }); + }; + + let hide_app = |_| { + sycamore::futures::spawn_local(async move { + let res = app::hide().await; + + log::debug!("app hide res {:?}", res); + }); + }; + + let get_name = |_| { + sycamore::futures::spawn_local(async move { + let res = app::get_name().await; + + log::debug!("app name {:?}", res); + }); + }; + + let get_version = |_| { + sycamore::futures::spawn_local(async move { + let res = app::get_version().await; + + log::debug!("app version {:?}", res); + }); + }; + + let get_tauri_version = |_| { + sycamore::futures::spawn_local(async move { + let res = app::get_tauri_version().await; + + log::debug!("tauri version {:?}", res); + }); + }; + + view! { cx, + div { + button(class="btn",id="get_name",on:click=get_name) { + "Get App Name" + } + button(class="btn",id="get_version",on:click=get_version) { + "Get App Version" + } + button(class="btn",id="get_tauri_version",on:click=get_tauri_version) { + "Get Tauri Version" + } + } + div { + button(class="btn",id="show",title="Hides and shows the app after 2 seconds",on:click=show_app) { + "Show" + } + button(class="btn",id="hide",on:click=hide_app) { + "Hide" + } + } + } +} diff --git a/examples/api/src/views/clipboard.rs b/examples/api/src/views/clipboard.rs new file mode 100644 index 0000000..ab18594 --- /dev/null +++ b/examples/api/src/views/clipboard.rs @@ -0,0 +1,42 @@ +use sycamore::prelude::*; +use tauri_api::clipboard::{read_text, write_text}; + +#[component] +pub fn Clipboard(cx: Scope) -> View { + let text = create_signal(cx, "clipboard message".to_string()); + + let write = move |_| { + sycamore::futures::spawn_local_scoped(cx, async move { + write_text(&text.get()).await + // .then(() => { + // onMessage('Wrote to the clipboard') + // }) + // .catch(onMessage) + }); + }; + + let read = |_| { + sycamore::futures::spawn_local(async move { + let text = read_text().await; + + log::info!("Read text from clipboard {:?}", text); + // readText() + // .then((contents) => { + // onMessage(`Clipboard contents: ${contents}`) + // }) + // .catch(onMessage) + }); + }; + + view! { cx, + div(class="flex gap-1") { + input(class="grow input",placeholder="Text to write to the clipboard",bind:value=text) + button(class="btn",type="button",on:click=write) { + "Write" + } + button(class="btn",type="button",on:click=read) { + "Read" + } + } + } +} diff --git a/examples/api/src/views/communication.rs b/examples/api/src/views/communication.rs new file mode 100644 index 0000000..526372b --- /dev/null +++ b/examples/api/src/views/communication.rs @@ -0,0 +1,90 @@ +use serde::{Deserialize, Serialize}; +use sycamore::prelude::*; +use tauri_api::event::{emit, listen}; +use tauri_api::tauri::invoke; +use shared::RequestBody; + +#[component] +pub fn Communication<'a, G: Html>(cx: Scope<'a>) -> View { + let unlisten = create_signal::>>(cx, None); + + // on_mount(cx, move || { + + // sycamore::futures::spawn_local_scoped(cx, async move { + // let unlisten_raw = listen::("rust-event", &|reply| log::debug!("got reply {:?}", reply)).await; + + // unlisten.set(Some(Box::new(&unlisten_raw))); + // }); + // }); + + // on_cleanup(cx, || { + // if let Some(unlisten) = unlisten .take().as_deref() { + // (unlisten)() + // } + // }); + + let log = |_| { + #[derive(Serialize)] + struct Payload<'a> { + event: &'a str, + payload: &'a str, + } + + sycamore::futures::spawn_local(async move { + let res = invoke::<_, ()>( + "log_operation", + &Payload { + event: "tauri-click", + payload: "this payload is optional because we used Option in Rust", + }, + ) + .await; + + log::debug!("Emitted event, response {:?}", res); + }); + }; + + let perform_request = |_| { + sycamore::futures::spawn_local(async move { + #[derive(Serialize)] + struct Payload<'a> { + endpoint: &'a str, + body: RequestBody<'a> + } + + let res = invoke::<_, String>( + "perform_request", + &Payload { + endpoint: "dummy endpoint arg", + body: RequestBody { + id: 5, + name: "test", + }, + }, + ) + .await; + + log::debug!("Got reply {:?}", res); + }); + }; + + let emit_event = |_| { + sycamore::futures::spawn_local(async move { + emit("js-event", &"this is the payload string").await; + }); + }; + + view! { cx, + div { + button(class="btn",id="log",on:click=log) { + "Call Log API" + } + button(class="btn",mid="request",on:click=perform_request) { + "Call Request (async) API" + } + button(class="btn",id="event",on:click=emit_event) { + "Send event to Rust" + } + } + } +} diff --git a/examples/api/src/views/mod.rs b/examples/api/src/views/mod.rs new file mode 100644 index 0000000..88f6dca --- /dev/null +++ b/examples/api/src/views/mod.rs @@ -0,0 +1,29 @@ +mod app; +mod clipboard; +mod communication; +mod welcome; + +use sycamore::view::View; +use sycamore_router::Route; +use sycamore::prelude::*; + +#[derive(Debug, Clone, Route)] +pub enum Page { + #[to("/app")] + App, + #[to("/clipboard")] + Clipboard, + #[to("/communication")] + Communication, + #[not_found] + NotFound +} + +pub fn switch(cx: Scope, route: &ReadSignal) -> View { + match route.get().as_ref() { + Page::App => app::App(cx), + Page::Clipboard => clipboard::Clipboard(cx), + Page::Communication => communication::Communication(cx), + Page::NotFound => welcome::Welcome(cx) + } +} \ No newline at end of file diff --git a/examples/api/src/views/welcome.rs b/examples/api/src/views/welcome.rs new file mode 100644 index 0000000..1fffd14 --- /dev/null +++ b/examples/api/src/views/welcome.rs @@ -0,0 +1,10 @@ +use sycamore::prelude::*; + +#[component] +pub fn Welcome(cx: Scope) -> View { + view! { cx, + h1 { + "Welcome" + } + } +} \ No newline at end of file diff --git a/examples/api/style.css b/examples/api/style.css new file mode 100644 index 0000000..a3fbd74 --- /dev/null +++ b/examples/api/style.css @@ -0,0 +1,120 @@ +.logo.yew:hover { + filter: drop-shadow(0 0 2em #20a88a); +} +.logo.sycamore { + color: #0000; + font-size: 3rem; + line-height: 1; + font-weight: 800; + -webkit-background-clip: text; + background-clip: text; + background-image: linear-gradient(to right,#fdba74, #f87171); + font-family: Inter,system-ui,sans-serif; +} +.logo.sycamore:hover { + filter: drop-shadow(0 0 2em #f87171); +} + +:root { + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + + color: #0f0f0f; + background-color: #f6f6f6; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +.container { + margin: 0; + padding-top: 10vh; + display: flex; + flex-direction: column; + justify-content: center; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: 0.75s; +} + +.logo.tauri:hover { + filter: drop-shadow(0 0 2em #24c8db); +} + +.row { + display: flex; + justify-content: center; + align-items: center; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +h1 { + text-align: center; +} + +input, +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + color: #0f0f0f; + background-color: #ffffff; + transition: border-color 0.25s; + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); +} + +button { + cursor: pointer; +} + +button:hover { + border-color: #396cd8; +} + +input, +button { + outline: none; +} + +#greet-input { + margin-right: 5px; +} + +@media (prefers-color-scheme: dark) { + :root { + color: #f6f6f6; + background-color: #2f2f2f; + } + + a:hover { + color: #24c8db; + } + + input, + button { + color: #ffffff; + background-color: #0f0f0f98; + } +} diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..c216daf --- /dev/null +++ b/src/app.rs @@ -0,0 +1,83 @@ +use semver::Version; + +/// Gets the application name. +/// +/// # Example +/// +/// ```typescript +/// import { getName } from '@tauri-apps/api/app'; +/// const appName = await getName(); +/// ``` +#[inline(always)] +pub async fn get_name() -> String { + inner::getName().await.as_string().unwrap() +} + +/// Gets the application version. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_api::app::get_version; +/// +/// let version = get_version().await; +/// ``` +#[inline(always)] +pub async fn get_version() -> Version { + Version::parse(&inner::getVersion().await.as_string().unwrap()).unwrap() +} + +/// Gets the Tauri version. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_app::app:get_tauri_version; +/// +/// let version = get_tauri_version().await; +/// ``` +#[inline(always)] +pub async fn get_tauri_version() -> Version { + Version::parse(&inner::getTauriVersion().await.as_string().unwrap()).unwrap() +} + +/// Shows the application on macOS. This function does not automatically focuses any app window. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_api::app::show; +/// +/// show().await; +/// ``` +#[inline(always)] +pub async fn show() { + inner::show().await; +} + +/// Hides the application on macOS. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_api::app::hide; +/// +/// hide().await; +/// ``` +#[inline(always)] +pub async fn hide() { + inner::hide().await; +} + +mod inner { + use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; + + #[wasm_bindgen(module = "/dist/app.js")] + extern "C" { + pub async fn getName() -> JsValue; + pub async fn getTauriVersion() -> JsValue; + pub async fn getVersion() -> JsValue; + pub async fn hide(); + pub async fn show(); + } +} \ No newline at end of file diff --git a/src/clipboard.rs b/src/clipboard.rs new file mode 100644 index 0000000..41049fc --- /dev/null +++ b/src/clipboard.rs @@ -0,0 +1,40 @@ +/// Gets the clipboard content as plain text. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_api::clipboard::read_text; +/// +/// let clipboard_text = read_text().await; +/// ``` +#[inline(always)] +pub async fn read_text() -> Option { + inner::readText().await.as_string() +} + +/// Writes plain text to the clipboard. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_api::clipboard::{write_text, read_text}; +/// +/// write_text("Tauri is awesome!").await; +/// assert_eq!(read_text().await, "Tauri is awesome!"); +/// ``` +/// +/// @returns A promise indicating the success or failure of the operation. +#[inline(always)] +pub async fn write_text(text: &str) { + inner::writeText(text).await +} + +mod inner { + use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; + + #[wasm_bindgen(module = "/dist/clipboard.js")] + extern "C" { + pub async fn readText() -> JsValue; + pub async fn writeText(text: &str); + } +} \ No newline at end of file diff --git a/src/event.rs b/src/event.rs new file mode 100644 index 0000000..73b0302 --- /dev/null +++ b/src/event.rs @@ -0,0 +1,134 @@ +use std::fmt::Debug; + +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +#[derive(Deserialize)] +pub struct Event { + /// Event name + pub event: String, + /// Event identifier used to unlisten + pub id: u32, + /// Event payload + pub payload: T, + /// The label of the window that emitted this event + pub window_label: String, +} + +impl Debug for Event { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Event") + .field("event", &self.event) + .field("id", &self.id) + .field("payload", &self.payload) + .field("window_label", &self.window_label) + .finish() + } +} + +/// Emits an event to the backend. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_api::event::emit; +/// use serde::Serialize; +/// +/// #[derive(Serialize)] +/// struct Payload { +/// logged_in: bool, +/// token: String +/// } +/// +/// emit("frontend-loaded", &Payload { logged_in: true, token: "authToken" }).await; +/// ``` +/// +/// @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. +#[inline(always)] +pub async fn emit(event: &str, payload: &T) { + inner::emit(event, serde_wasm_bindgen::to_value(payload).unwrap()).await +} + +/// Listen to an event from the backend. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_api::event::{emit, listen}; +/// +/// const unlisten = listen::("error", |event| { +/// println!("Got error in window {}, payload: {}", event.window_label, event.payload); +/// }).await; +/// +/// // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted +/// unlisten(); +/// ``` +/// +/// @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. +/// @param handler Event handler callback. +/// @returns A promise resolving to a function to unlisten to the event. +/// +/// Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. +#[inline(always)] +pub async fn listen(event: &str, handler: &dyn Fn(Event)) -> impl FnOnce() { + let unlisten = inner::listen(event, &|raw| { + handler(serde_wasm_bindgen::from_value(raw).unwrap()) + }) + .await; + + let unlisten = js_sys::Function::from(unlisten); + move || { + unlisten.call0(&wasm_bindgen::JsValue::NULL).unwrap(); + } +} + +/// Listen to an one-off event from the backend. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_api::event::once; +/// use serde::Deserialize; +/// +/// #[derive(Deserialize)] +/// interface LoadedPayload { +/// logged_in: bool, +/// token: String +/// } +/// const unlisten = once::("loaded", |event| { +/// println!("App is loaded, loggedIn: {}, token: {}", event.payload.logged_in, event.payload.token); +/// }).await; +/// +/// // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted +/// unlisten(); +/// ``` +/// +/// @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. +/// @returns A promise resolving to a function to unlisten to the event. +/// +/// Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. +#[inline(always)] +pub async fn once( + event: &str, + handler: &mut dyn FnMut(Event), +) -> impl FnOnce() { + let unlisten = inner::once(event, &mut |raw| { + handler(serde_wasm_bindgen::from_value(raw).unwrap()) + }) + .await; + + let unlisten = js_sys::Function::from(unlisten); + move || { + unlisten.call0(&wasm_bindgen::JsValue::NULL).unwrap(); + } +} + +mod inner { + use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; + + #[wasm_bindgen(module = "/dist/event.js")] + extern "C" { + pub async fn emit(event: &str, payload: JsValue); + pub async fn listen(event: &str, handler: &dyn Fn(JsValue)) -> JsValue; + pub async fn once(event: &str, handler: &mut dyn FnMut(JsValue)) -> JsValue; + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..aeb8a19 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,22 @@ +use wasm_bindgen::JsValue; + +#[cfg(feature = "app")] +pub mod app; +#[cfg(feature = "clipboard")] +pub mod clipboard; +#[cfg(feature = "event")] +pub mod event; +#[cfg(feature = "mocks")] +pub mod mocks; +#[cfg(feature = "tauri")] +pub mod tauri; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Serde(#[from] serde_wasm_bindgen::Error), + #[error("{0:?}")] + Other(JsValue) +} + +pub(crate) type Result = std::result::Result; \ No newline at end of file diff --git a/src/mocks.rs b/src/mocks.rs new file mode 100644 index 0000000..ff766ea --- /dev/null +++ b/src/mocks.rs @@ -0,0 +1,54 @@ +use js_sys::Array; +use wasm_bindgen::JsValue; + +/// Mocks the current window label +/// In non-tauri context it is required to call this function///before* using the `@tauri-apps/api/window` module. +/// +/// This function only mocks the *presence* of a window, +/// window properties (e.g. width and height) can be mocked like regular IPC calls using the `mockIPC` function. +pub fn mock_window(current: &str) { + inner::mockWindows( + current, + JsValue::UNDEFINED, + ) +} + +/// Mocks many window labels. +/// In non-tauri context it is required to call this function///before* using the `@tauri-apps/api/window` module. +/// +/// This function only mocks the *presence* of windows, +/// window properties (e.g. width and height) can be mocked like regular IPC calls using the `mockIPC` function. +/// +/// @param current Label of window this JavaScript context is running in. +/// @param additionalWindows Label of additional windows the app has. +pub fn mock_windows(current: &str, additional_windows: &[&str]) { + inner::mockWindows( + current, + Array::from_iter(additional_windows.iter().map(|str| JsValue::from_str(str))).into(), + ) +} + +/// Intercepts all IPC requests with the given mock handler. +/// +/// This function can be used when testing tauri frontend applications or when running the frontend in a Node.js context during static site generation. +pub fn mock_ipc(handler: &dyn Fn(JsValue)) { + inner::mockIPC(handler); +} + +/// Clears mocked functions/data injected by the other functions in this module. +/// When using a test runner that doesn't provide a fresh window object for each test, calling this function will reset tauri specific properties. +pub fn clear_mocks() { + inner::clearMocks() +} + +mod inner { + use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; + + #[wasm_bindgen(module = "/dist/mocks.js")] + extern "C" { + #[wasm_bindgen(variadic)] + pub fn mockWindows(current: &str, rest: JsValue); + pub fn mockIPC(handler: &dyn Fn(JsValue)); + pub fn clearMocks(); + } +} diff --git a/src/tauri.rs b/src/tauri.rs new file mode 100644 index 0000000..ad43df3 --- /dev/null +++ b/src/tauri.rs @@ -0,0 +1,100 @@ +use serde::{de::DeserializeOwned, Serialize}; +use url::Url; + +/// Convert a device file path to an URL that can be loaded by the webview. +/// Note that `asset:` and `https://asset.localhost` must be added to [`tauri.security.csp`](https://tauri.app/v1/api/config/#securityconfig.csp) in `tauri.conf.json`. +/// Example CSP value: `"csp": "default-src 'self'; img-src 'self' asset: https://asset.localhost"` to use the asset protocol on image sources. +/// +/// Additionally, `asset` must be added to [`tauri.allowlist.protocol`](https://tauri.app/v1/api/config/#allowlistconfig.protocol) +/// in `tauri.conf.json` and its access scope must be defined on the `assetScope` array on the same `protocol` object. +/// +/// @param filePath The file path. +/// @param protocol The protocol to use. Defaults to `asset`. You only need to set this when using a custom protocol. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_api::path::{app_data_dir, join}; +/// use tauri_api::tauri::convert_file_src; +/// +/// const app_data_dir_path = app_data_dir().await; +/// const file_path = join(app_data_dir_path, "assets/video.mp4").await; +/// const asset_url = convert_file_src(file_path); +/// +/// let window = web_sys::window().expect("no global `window` exists"); +/// let document = window.document().expect("should have a document on window"); +/// +/// // Manufacture the element we're gonna append +/// let video = document.get_element_by_id("my-video")?; +/// let source = document.create_element("source")?; +/// +/// source.set_attribute("type", "video/mp4")?; +/// source.set_attribute("src", asset_url.as_str())?; +/// +/// video.append_child(&val)?; +/// ``` +/// +/// @return the URL that can be used as source on the webview. +#[inline(always)] +pub async fn convert_file_src(file_path: &str, protocol: Option<&str>) -> Url { + Url::parse( + &inner::convertFileSrc(file_path, protocol) + .await + .as_string() + .unwrap(), + ) + .unwrap() +} + +/// Sends a message to the backend. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_api::tauri::invoke; +/// +/// struct User<'a> { +/// user: &'a str, +/// password: &'a str +/// } +/// +/// invoke("login", &User { user: "tauri", password: "poiwe3h4r5ip3yrhtew9ty" }).await; +/// ``` +/// +/// @param cmd The command name. +/// @param args The optional arguments to pass to the command. +/// @return A promise resolving or rejecting to the backend response. +#[inline(always)] +pub async fn invoke(cmd: &str, args: &A) -> crate::Result { + let res = inner::invoke(cmd, serde_wasm_bindgen::to_value(args).unwrap()).await; + + let raw = res.map_err(crate::Error::Other)?; + serde_wasm_bindgen::from_value(raw).map_err(Into::into) +} + +/// Transforms a callback function to a string identifier that can be passed to the backend. +/// The backend uses the identifier to `eval()` the callback. +/// +/// @return A unique identifier associated with the callback function. +#[inline(always)] +pub async fn transform_callback(callback: &dyn Fn(T), once: bool) -> f64 { + inner::transformCallback( + &|raw| callback(serde_wasm_bindgen::from_value(raw).unwrap()), + once, + ) + .await + .as_f64() + .unwrap() +} + +mod inner { + use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; + + #[wasm_bindgen(module = "/dist/tauri.js")] + extern "C" { + pub async fn convertFileSrc(filePath: &str, protocol: Option<&str>) -> JsValue; + #[wasm_bindgen(catch)] + pub async fn invoke(cmd: &str, args: JsValue) -> Result; + pub async fn transformCallback(callback: &dyn Fn(JsValue), once: bool) -> JsValue; + } +} diff --git a/tauri b/tauri new file mode 160000 index 0000000..35264b4 --- /dev/null +++ b/tauri @@ -0,0 +1 @@ +Subproject commit 35264b4c1801b381e0b867c1c35540f0fbb43365