copy plugin sources

This commit is contained in:
Jonas Kruckenberg
2022-12-14 18:43:39 +01:00
parent c15990d4e2
commit eb3495fb72
145 changed files with 16069 additions and 22 deletions
+24
View File
@@ -0,0 +1,24 @@
[package]
name = "tauri-bindgen-host-macro"
authors.workspace = true
version.workspace = true
edition.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde.workspace = true
serde_json.workspace = true
tauri.workspace = true
log.workspace = true
thiserror.workspace = true
authenticator = "0.3.1"
once_cell = "1.9"
sha2 = "0.10"
base64 = { version = "^0.13" }
u2f = "0.2"
chrono = "0.4"
[dev-dependencies]
rand = "0.8"
rusty-fork = "0.3"
+45
View File
@@ -0,0 +1,45 @@
var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumerable:!0});};
var f={};e(f,{convertFileSrc:()=>w,invoke:()=>c,transformCallback:()=>s});function u(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function s(e,r=!1){let n=u(),t=`_${n}`;return Object.defineProperty(window,t,{value:o=>(r&&Reflect.deleteProperty(window,t),e==null?void 0:e(o)),writable:!1,configurable:!0}),n}async function c(e,r={}){return new Promise((n,t)=>{let o=s(i=>{n(i),Reflect.deleteProperty(window,`_${a}`);},!0),a=s(i=>{t(i),Reflect.deleteProperty(window,`_${o}`);},!0);window.__TAURI_IPC__({cmd:e,callback:o,error:a,...r});})}function w(e,r="asset"){let n=encodeURIComponent(e);return navigator.userAgent.includes("Windows")?`https://${r}.localhost/${n}`:`${r}://localhost/${n}`}
class Authenticator {
async init() {
return await c("plugin:authenticator|init");
}
async register(challenge, application) {
return await c("plugin:authenticator|register", {
timeout: 10000,
challenge,
application,
});
}
async verifyRegistration(challenge, application, registerData, clientData) {
return await c("plugin:authenticator|verify_registration", {
challenge,
application,
registerData,
clientData,
});
}
async sign(challenge, application, keyHandle) {
return await c("plugin:authenticator|sign", {
timeout: 10000,
challenge,
application,
keyHandle,
});
}
async verifySignature(challenge, application, signData, clientData, keyHandle, pubkey) {
return await c("plugin:authenticator|verify_signature", {
challenge,
application,
signData,
clientData,
keyHandle,
pubkey,
});
}
}
export { Authenticator };
//# sourceMappingURL=index.min.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.min.js","sources":["../../../../node_modules/.pnpm/@tauri-apps+api@1.2.0/node_modules/@tauri-apps/api/chunk-FEIY7W7S.js","../../../../node_modules/.pnpm/@tauri-apps+api@1.2.0/node_modules/@tauri-apps/api/chunk-RCPA6UVN.js","../index.ts"],"sourcesContent":["var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumerable:!0})};export{e as a};\n","import{a as d}from\"./chunk-FEIY7W7S.js\";var f={};d(f,{convertFileSrc:()=>w,invoke:()=>c,transformCallback:()=>s});function u(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function s(e,r=!1){let n=u(),t=`_${n}`;return Object.defineProperty(window,t,{value:o=>(r&&Reflect.deleteProperty(window,t),e==null?void 0:e(o)),writable:!1,configurable:!0}),n}async function c(e,r={}){return new Promise((n,t)=>{let o=s(i=>{n(i),Reflect.deleteProperty(window,`_${a}`)},!0),a=s(i=>{t(i),Reflect.deleteProperty(window,`_${o}`)},!0);window.__TAURI_IPC__({cmd:e,callback:o,error:a,...r})})}function w(e,r=\"asset\"){let n=encodeURIComponent(e);return navigator.userAgent.includes(\"Windows\")?`https://${r}.localhost/${n}`:`${r}://localhost/${n}`}export{s as a,c as b,w as c,f as d};\n",null],"names":["d","invoke"],"mappings":"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"}
+43
View File
@@ -0,0 +1,43 @@
import { invoke } from '@tauri-apps/api/tauri';
class Authenticator {
async init() {
return await invoke("plugin:authenticator|init");
}
async register(challenge, application) {
return await invoke("plugin:authenticator|register", {
timeout: 10000,
challenge,
application,
});
}
async verifyRegistration(challenge, application, registerData, clientData) {
return await invoke("plugin:authenticator|verify_registration", {
challenge,
application,
registerData,
clientData,
});
}
async sign(challenge, application, keyHandle) {
return await invoke("plugin:authenticator|sign", {
timeout: 10000,
challenge,
application,
keyHandle,
});
}
async verifySignature(challenge, application, signData, clientData, keyHandle, pubkey) {
return await invoke("plugin:authenticator|verify_signature", {
challenge,
application,
signData,
clientData,
keyHandle,
pubkey,
});
}
}
export { Authenticator };
//# sourceMappingURL=index.mjs.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.mjs","sources":["../index.ts"],"sourcesContent":[null],"names":[],"mappings":";;MAEa,aAAa,CAAA;AACxB,IAAA,MAAM,IAAI,GAAA;AACR,QAAA,OAAO,MAAM,MAAM,CAAC,2BAA2B,CAAC,CAAC;KAClD;AAED,IAAA,MAAM,QAAQ,CAAC,SAAiB,EAAE,WAAmB,EAAA;AACnD,QAAA,OAAO,MAAM,MAAM,CAAC,+BAA+B,EAAE;AACnD,YAAA,OAAO,EAAE,KAAK;YACd,SAAS;YACT,WAAW;AACZ,SAAA,CAAC,CAAC;KACJ;IAED,MAAM,kBAAkB,CACtB,SAAiB,EACjB,WAAmB,EACnB,YAAoB,EACpB,UAAkB,EAAA;AAElB,QAAA,OAAO,MAAM,MAAM,CAAC,0CAA0C,EAAE;YAC9D,SAAS;YACT,WAAW;YACX,YAAY;YACZ,UAAU;AACX,SAAA,CAAC,CAAC;KACJ;AAED,IAAA,MAAM,IAAI,CACR,SAAiB,EACjB,WAAmB,EACnB,SAAiB,EAAA;AAEjB,QAAA,OAAO,MAAM,MAAM,CAAC,2BAA2B,EAAE;AAC/C,YAAA,OAAO,EAAE,KAAK;YACd,SAAS;YACT,WAAW;YACX,SAAS;AACV,SAAA,CAAC,CAAC;KACJ;AAED,IAAA,MAAM,eAAe,CACnB,SAAiB,EACjB,WAAmB,EACnB,QAAgB,EAChB,UAAkB,EAClB,SAAiB,EACjB,MAAc,EAAA;AAEd,QAAA,OAAO,MAAM,MAAM,CAAC,uCAAuC,EAAE;YAC3D,SAAS;YACT,WAAW;YACX,QAAQ;YACR,UAAU;YACV,SAAS;YACT,MAAM;AACP,SAAA,CAAC,CAAC;KACJ;AACF;;;;"}
+60
View File
@@ -0,0 +1,60 @@
import { invoke } from "@tauri-apps/api/tauri";
export class Authenticator {
async init(): Promise<void> {
return await invoke("plugin:authenticator|init");
}
async register(challenge: string, application: string): Promise<string> {
return await invoke("plugin:authenticator|register", {
timeout: 10000,
challenge,
application,
});
}
async verifyRegistration(
challenge: string,
application: string,
registerData: string,
clientData: string
): Promise<string> {
return await invoke("plugin:authenticator|verify_registration", {
challenge,
application,
registerData,
clientData,
});
}
async sign(
challenge: string,
application: string,
keyHandle: string
): Promise<string> {
return await invoke("plugin:authenticator|sign", {
timeout: 10000,
challenge,
application,
keyHandle,
});
}
async verifySignature(
challenge: string,
application: string,
signData: string,
clientData: string,
keyHandle: string,
pubkey: string
): Promise<number> {
return await invoke("plugin:authenticator|verify_signature", {
challenge,
application,
signData,
clientData,
keyHandle,
pubkey,
});
}
}
@@ -0,0 +1,29 @@
{
"name": "tauri-plugin-authenticator-api",
"version": "0.0.0",
"license": "MIT or APACHE-2.0",
"type": "module",
"browser": "dist/index.min.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"browser": "./dist/index.min.js"
},
"scripts": {
"build": "rollup -c"
},
"files": [
"dist",
"!dist/**/*.map",
"README.md",
"LICENSE"
],
"devDependencies": {
"tslib": "^2.4.1"
},
"dependencies": {
"@tauri-apps/api": "^1.2.0"
}
}
@@ -0,0 +1,10 @@
import { readFileSync } from "fs";
import { createConfig } from "../../../shared/rollup.config.mjs";
export default createConfig({
pkg: JSON.parse(
readFileSync(new URL("./package.json", import.meta.url), "utf8")
),
external: [/^@tauri-apps\/api/],
});
+1
View File
@@ -0,0 +1 @@
../../../shared/tsconfig.json
+217
View File
@@ -0,0 +1,217 @@
// Copyright 2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use authenticator::{
authenticatorservice::AuthenticatorService, statecallback::StateCallback,
AuthenticatorTransports, KeyHandle, RegisterFlags, SignFlags, StatusUpdate,
};
use base64::{decode_config, encode_config, URL_SAFE_NO_PAD};
use once_cell::sync::Lazy;
use serde::Serialize;
use sha2::{Digest, Sha256};
use std::io;
use std::sync::mpsc::channel;
use std::{convert::Into, sync::Mutex};
static MANAGER: Lazy<Mutex<AuthenticatorService>> = Lazy::new(|| {
let manager = AuthenticatorService::new().expect("The auth service should initialize safely");
Mutex::new(manager)
});
pub fn init_usb() {
let mut manager = MANAGER.lock().unwrap();
// theres also "add_detected_transports()" in the docs?
manager.add_u2f_usb_hid_platform_transports();
}
#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Registration {
pub key_handle: String,
pub pubkey: String,
pub register_data: String,
pub client_data: String,
}
pub fn register(application: String, timeout: u64, challenge: String) -> crate::Result<String> {
let (chall_bytes, app_bytes, client_data_string) =
format_client_data(application.as_str(), challenge.as_str());
// log the status rx?
let (status_tx, _status_rx) = channel::<StatusUpdate>();
let mut manager = MANAGER.lock().unwrap();
let (register_tx, register_rx) = channel();
let callback = StateCallback::new(Box::new(move |rv| {
register_tx.send(rv).unwrap();
}));
let res = manager.register(
RegisterFlags::empty(),
timeout,
chall_bytes,
app_bytes,
vec![],
status_tx,
callback,
);
match res {
Ok(_r) => {
let register_result = register_rx
.recv()
.expect("Problem receiving, unable to continue");
if let Err(e) = register_result {
return Err(e.into());
}
let (register_data, device_info) = register_result.unwrap(); // error already has been checked
// println!("Register result: {}", base64::encode(&register_data));
println!("Device info: {}", &device_info);
let (key_handle, public_key) =
_u2f_get_key_handle_and_public_key_from_register_response(&register_data).unwrap();
let key_handle_base64 = encode_config(&key_handle, URL_SAFE_NO_PAD);
let public_key_base64 = encode_config(&public_key, URL_SAFE_NO_PAD);
let register_data_base64 = encode_config(&register_data, URL_SAFE_NO_PAD);
println!("Key Handle: {}", &key_handle_base64);
println!("Public Key: {}", &public_key_base64);
// Ok(base64::encode(&register_data))
// Ok(key_handle_base64)
let res = serde_json::to_string(&Registration {
key_handle: key_handle_base64,
pubkey: public_key_base64,
register_data: register_data_base64,
client_data: client_data_string,
})?;
Ok(res)
}
Err(e) => Err(e.into()),
}
}
#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Signature {
pub key_handle: String,
pub sign_data: String,
}
pub fn sign(
application: String,
timeout: u64,
challenge: String,
key_handle: String,
) -> crate::Result<String> {
let credential = match decode_config(&key_handle, URL_SAFE_NO_PAD) {
Ok(v) => v,
Err(e) => {
return Err(e.into());
}
};
let key_handle = KeyHandle {
credential,
transports: AuthenticatorTransports::empty(),
};
let (chall_bytes, app_bytes, _) = format_client_data(application.as_str(), challenge.as_str());
let (sign_tx, sign_rx) = channel();
let callback = StateCallback::new(Box::new(move |rv| {
sign_tx.send(rv).unwrap();
}));
// log the status rx?
let (status_tx, _status_rx) = channel::<StatusUpdate>();
let mut manager = MANAGER.lock().unwrap();
let res = manager.sign(
SignFlags::empty(),
timeout,
chall_bytes,
vec![app_bytes],
vec![key_handle],
status_tx,
callback,
);
match res {
Ok(_v) => {
let sign_result = sign_rx
.recv()
.expect("Problem receiving, unable to continue");
if let Err(e) = sign_result {
return Err(e.into());
}
let (_, handle_used, sign_data, device_info) = sign_result.unwrap();
let sig = encode_config(&sign_data, URL_SAFE_NO_PAD);
println!("Sign result: {}", sig);
println!(
"Key handle used: {}",
encode_config(&handle_used, URL_SAFE_NO_PAD)
);
println!("Device info: {}", &device_info);
println!("Done.");
let res = serde_json::to_string(&Signature {
sign_data: sig,
key_handle: encode_config(&handle_used, URL_SAFE_NO_PAD),
})?;
Ok(res)
}
Err(e) => Err(e.into()),
}
}
fn format_client_data(application: &str, challenge: &str) -> (Vec<u8>, Vec<u8>, String) {
let d = format!(
r#"{{"challenge": "{}", "version": "U2F_V2", "appId": "{}"}}"#,
challenge, application
);
let mut challenge = Sha256::new();
challenge.update(d.as_bytes());
let chall_bytes = challenge.finalize().to_vec();
let mut app = Sha256::new();
app.update(application.as_bytes());
let app_bytes = app.finalize().to_vec();
(chall_bytes, app_bytes, d)
}
fn _u2f_get_key_handle_and_public_key_from_register_response(
register_response: &[u8],
) -> io::Result<(Vec<u8>, Vec<u8>)> {
if register_response[0] != 0x05 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Reserved byte not set correctly",
));
}
// 1: reserved
// 65: public key
// 1: key handle length
// key handle
// x.509 cert
// sig
let key_handle_len = register_response[66] as usize;
let mut public_key = register_response.to_owned();
let mut key_handle = public_key.split_off(67);
let _attestation = key_handle.split_off(key_handle_len);
// remove fist (reserved) and last (handle len) bytes
let pk: Vec<u8> = public_key[1..public_key.len() - 1].to_vec();
Ok((key_handle, pk))
}
+22
View File
@@ -0,0 +1,22 @@
use serde::{Serialize, Serializer};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Base64Decode(#[from] base64::DecodeError),
#[error(transparent)]
JSON(#[from] serde_json::Error),
#[error(transparent)]
U2F(#[from] u2f::u2ferror::U2fError),
#[error(transparent)]
Auth(#[from] authenticator::errors::AuthenticatorError),
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
+89
View File
@@ -0,0 +1,89 @@
// Copyright 2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
mod auth;
mod error;
mod u2f;
use tauri::{plugin::Plugin, Invoke, Runtime};
pub use error::Error;
type Result<T> = std::result::Result<T, Error>;
#[tauri::command]
fn init() {
auth::init_usb();
}
#[tauri::command]
fn register(timeout: u64, challenge: String, application: String) -> crate::Result<String> {
auth::register(application, timeout, challenge)
}
#[tauri::command]
fn verify_registration(
challenge: String,
application: String,
register_data: String,
client_data: String,
) -> crate::Result<String> {
u2f::verify_registration(application, challenge, register_data, client_data)
}
#[tauri::command]
fn sign(
timeout: u64,
challenge: String,
application: String,
key_handle: String,
) -> crate::Result<String> {
auth::sign(application, timeout, challenge, key_handle)
}
#[tauri::command]
fn verify_signature(
challenge: String,
application: String,
sign_data: String,
client_data: String,
key_handle: String,
pubkey: String,
) -> crate::Result<u32> {
u2f::verify_signature(
application,
challenge,
sign_data,
client_data,
key_handle,
pubkey,
)
}
pub struct TauriAuthenticator<R: Runtime> {
invoke_handler: Box<dyn Fn(Invoke<R>) + Send + Sync>,
}
impl<R: Runtime> Default for TauriAuthenticator<R> {
fn default() -> Self {
Self {
invoke_handler: Box::new(tauri::generate_handler![
init,
register,
verify_registration,
sign,
verify_signature
]),
}
}
}
impl<R: Runtime> Plugin<R> for TauriAuthenticator<R> {
fn name(&self) -> &'static str {
"authenticator"
}
fn extend_api(&mut self, invoke: Invoke<R>) {
(self.invoke_handler)(invoke)
}
}
+105
View File
@@ -0,0 +1,105 @@
// Copyright 2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use base64::{decode_config, encode_config, URL_SAFE_NO_PAD};
use chrono::prelude::*;
use serde::Serialize;
use std::convert::Into;
use u2f::messages::*;
use u2f::protocol::*;
use u2f::register::*;
static VERSION: &str = "U2F_V2";
pub fn make_challenge(app_id: &str, challenge_bytes: Vec<u8>) -> Challenge {
let utc: DateTime<Utc> = Utc::now();
Challenge {
challenge: encode_config(&challenge_bytes, URL_SAFE_NO_PAD),
timestamp: format!("{:?}", utc),
app_id: app_id.to_string(),
}
}
#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct RegistrationVerification {
pub key_handle: String,
pub pubkey: String,
pub device_name: Option<String>,
}
pub fn verify_registration(
app_id: String,
challenge: String,
register_data: String,
client_data: String,
) -> crate::Result<String> {
let challenge_bytes = decode_config(&challenge, URL_SAFE_NO_PAD)?;
let challenge = make_challenge(&app_id, challenge_bytes);
let client_data_bytes: Vec<u8> = client_data.as_bytes().into();
let client_data_base64 = encode_config(&client_data_bytes, URL_SAFE_NO_PAD);
let client = U2f::new(app_id);
match client.register_response(
challenge,
RegisterResponse {
registration_data: register_data,
client_data: client_data_base64,
version: VERSION.to_string(),
},
) {
Ok(v) => {
let rv = RegistrationVerification {
key_handle: encode_config(&v.key_handle, URL_SAFE_NO_PAD),
pubkey: encode_config(&v.pub_key, URL_SAFE_NO_PAD),
device_name: v.device_name,
};
Ok(serde_json::to_string(&rv)?)
}
Err(e) => Err(e.into()),
}
}
#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SignatureVerification {
pub counter: u8,
}
pub fn verify_signature(
app_id: String,
challenge: String,
sign_data: String,
client_data: String,
key_handle: String,
pub_key: String,
) -> crate::Result<u32> {
let challenge_bytes = decode_config(&challenge, URL_SAFE_NO_PAD)?;
let chal = make_challenge(&app_id, challenge_bytes);
let client_data_bytes: Vec<u8> = client_data.as_bytes().into();
let client_data_base64 = encode_config(&client_data_bytes, URL_SAFE_NO_PAD);
let key_handle_bytes = decode_config(&key_handle, URL_SAFE_NO_PAD)?;
let pubkey_bytes = decode_config(&pub_key, URL_SAFE_NO_PAD)?;
let client = U2f::new(app_id);
let mut _counter: u32 = 0;
match client.sign_response(
chal,
Registration {
// here only needs pubkey and keyhandle
key_handle: key_handle_bytes,
pub_key: pubkey_bytes,
attestation_cert: None,
device_name: None,
},
SignResponse {
// here needs client data and sig data and key_handle
signature_data: sign_data,
client_data: client_data_base64,
key_handle,
},
_counter,
) {
Ok(v) => Ok(v),
Err(e) => Err(e.into()),
}
}
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "tauri-plugin-autostart"
version = "0.1.0"
edition.workspace = true
authors.workspace = true
license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde.workspace = true
serde_json.workspace = true
tauri.workspace = true
log.workspace = true
thiserror.workspace = true
auto-launch = "0.3"
+17
View File
@@ -0,0 +1,17 @@
var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumerable:!0});};
var f={};e(f,{convertFileSrc:()=>w,invoke:()=>c,transformCallback:()=>s});function u(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function s(e,r=!1){let n=u(),t=`_${n}`;return Object.defineProperty(window,t,{value:o=>(r&&Reflect.deleteProperty(window,t),e==null?void 0:e(o)),writable:!1,configurable:!0}),n}async function c(e,r={}){return new Promise((n,t)=>{let o=s(i=>{n(i),Reflect.deleteProperty(window,`_${a}`);},!0),a=s(i=>{t(i),Reflect.deleteProperty(window,`_${o}`);},!0);window.__TAURI_IPC__({cmd:e,callback:o,error:a,...r});})}function w(e,r="asset"){let n=encodeURIComponent(e);return navigator.userAgent.includes("Windows")?`https://${r}.localhost/${n}`:`${r}://localhost/${n}`}
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
async function isEnabled() {
return await c("plugin:autostart|is_enabled");
}
async function enable() {
await c("plugin:autostart|enable");
}
async function disable() {
await c("plugin:autostart|disable");
}
export { disable, enable, isEnabled };
//# sourceMappingURL=index.min.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.min.js","sources":["../../../../node_modules/.pnpm/@tauri-apps+api@1.2.0/node_modules/@tauri-apps/api/chunk-FEIY7W7S.js","../../../../node_modules/.pnpm/@tauri-apps+api@1.2.0/node_modules/@tauri-apps/api/chunk-RCPA6UVN.js","../index.ts"],"sourcesContent":["var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumerable:!0})};export{e as a};\n","import{a as d}from\"./chunk-FEIY7W7S.js\";var f={};d(f,{convertFileSrc:()=>w,invoke:()=>c,transformCallback:()=>s});function u(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function s(e,r=!1){let n=u(),t=`_${n}`;return Object.defineProperty(window,t,{value:o=>(r&&Reflect.deleteProperty(window,t),e==null?void 0:e(o)),writable:!1,configurable:!0}),n}async function c(e,r={}){return new Promise((n,t)=>{let o=s(i=>{n(i),Reflect.deleteProperty(window,`_${a}`)},!0),a=s(i=>{t(i),Reflect.deleteProperty(window,`_${o}`)},!0);window.__TAURI_IPC__({cmd:e,callback:o,error:a,...r})})}function w(e,r=\"asset\"){let n=encodeURIComponent(e);return navigator.userAgent.includes(\"Windows\")?`https://${r}.localhost/${n}`:`${r}://localhost/${n}`}export{s as a,c as b,w as c,f as d};\n",null],"names":["d","invoke"],"mappings":"AAAA,IAAI,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAC,CAAC;;ACAjD,IAAI,CAAC,CAAC,EAAE,CAACA,CAAC,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,CAAC,OAAO,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,EAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,OAAO,SAAS,CAAC,SAAS,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC;;ACAtuB;AAMO,eAAe,SAAS,GAAA;AAC7B,IAAA,OAAO,MAAMC,CAAM,CAAC,6BAA6B,CAAC,CAAC;AACrD,CAAC;AAEM,eAAe,MAAM,GAAA;AAC1B,IAAA,MAAMA,CAAM,CAAC,yBAAyB,CAAC,CAAC;AAC1C,CAAC;AAEM,eAAe,OAAO,GAAA;AAC3B,IAAA,MAAMA,CAAM,CAAC,0BAA0B,CAAC,CAAC;AAC3C;;;;"}
+15
View File
@@ -0,0 +1,15 @@
import { invoke } from '@tauri-apps/api/tauri';
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
async function isEnabled() {
return await invoke("plugin:autostart|is_enabled");
}
async function enable() {
await invoke("plugin:autostart|enable");
}
async function disable() {
await invoke("plugin:autostart|disable");
}
export { disable, enable, isEnabled };
//# sourceMappingURL=index.mjs.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.mjs","sources":["../index.ts"],"sourcesContent":[null],"names":[],"mappings":";;AAAA;AAMO,eAAe,SAAS,GAAA;AAC7B,IAAA,OAAO,MAAM,MAAM,CAAC,6BAA6B,CAAC,CAAC;AACrD,CAAC;AAEM,eAAe,MAAM,GAAA;AAC1B,IAAA,MAAM,MAAM,CAAC,yBAAyB,CAAC,CAAC;AAC1C,CAAC;AAEM,eAAe,OAAO,GAAA;AAC3B,IAAA,MAAM,MAAM,CAAC,0BAA0B,CAAC,CAAC;AAC3C;;;;"}
+17
View File
@@ -0,0 +1,17 @@
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import { invoke } from "@tauri-apps/api/tauri";
export async function isEnabled(): Promise<boolean> {
return await invoke("plugin:autostart|is_enabled");
}
export async function enable(): Promise<void> {
await invoke("plugin:autostart|enable");
}
export async function disable(): Promise<void> {
await invoke("plugin:autostart|disable");
}
+29
View File
@@ -0,0 +1,29 @@
{
"name": "tauri-plugin-autostart-api",
"version": "0.0.0",
"license": "MIT or APACHE-2.0",
"type": "module",
"browser": "dist/index.min.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"browser": "./dist/index.min.js"
},
"scripts": {
"build": "rollup -c"
},
"files": [
"dist",
"!dist/**/*.map",
"README.md",
"LICENSE"
],
"devDependencies": {
"tslib": "^2.4.1"
},
"dependencies": {
"@tauri-apps/api": "^1.2.0"
}
}
@@ -0,0 +1,10 @@
import { readFileSync } from "fs";
import { createConfig } from "../../../shared/rollup.config.mjs";
export default createConfig({
pkg: JSON.parse(
readFileSync(new URL("./package.json", import.meta.url), "utf8")
),
external: [/^@tauri-apps\/api/],
});
+1
View File
@@ -0,0 +1 @@
../../../shared/tsconfig.json
+131
View File
@@ -0,0 +1,131 @@
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use auto_launch::{AutoLaunch, AutoLaunchBuilder};
use serde::{ser::Serializer, Serialize};
use tauri::{
command,
plugin::{Builder, TauriPlugin},
Manager, Runtime, State,
};
use std::env::current_exe;
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Copy, Clone)]
pub enum MacosLauncher {
LaunchAgent,
AppleScript,
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("{0}")]
Anyhow(String),
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
pub struct AutoLaunchManager(AutoLaunch);
impl AutoLaunchManager {
pub fn enable(&self) -> Result<()> {
self.0
.enable()
.map_err(|e| e.to_string())
.map_err(Error::Anyhow)
}
pub fn disable(&self) -> Result<()> {
self.0
.disable()
.map_err(|e| e.to_string())
.map_err(Error::Anyhow)
}
pub fn is_enabled(&self) -> Result<bool> {
self.0
.is_enabled()
.map_err(|e| e.to_string())
.map_err(Error::Anyhow)
}
}
pub trait ManagerExt<R: Runtime> {
fn autolaunch(&self) -> State<'_, AutoLaunchManager>;
}
impl<R: Runtime, T: Manager<R>> ManagerExt<R> for T {
fn autolaunch(&self) -> State<'_, AutoLaunchManager> {
self.state::<AutoLaunchManager>()
}
}
#[command]
async fn enable(manager: State<'_, AutoLaunchManager>) -> Result<()> {
manager.enable()
}
#[command]
async fn disable(manager: State<'_, AutoLaunchManager>) -> Result<()> {
manager.disable()
}
#[command]
async fn is_enabled(manager: State<'_, AutoLaunchManager>) -> Result<bool> {
manager.is_enabled()
}
/// Initializes the plugin.
///
/// `args` - are passed to your app on startup.
pub fn init<R: Runtime>(
macos_launcher: MacosLauncher,
args: Option<Vec<&'static str>>,
) -> TauriPlugin<R> {
Builder::new("autostart")
.invoke_handler(tauri::generate_handler![enable, disable, is_enabled])
.setup(move |app| {
let mut builder = AutoLaunchBuilder::new();
builder.set_app_name(&app.package_info().name);
if let Some(args) = args {
builder.set_args(&args);
}
builder.set_use_launch_agent(matches!(macos_launcher, MacosLauncher::LaunchAgent));
let current_exe = current_exe()?;
#[cfg(windows)]
builder.set_app_path(&current_exe.display().to_string());
#[cfg(target_os = "macos")]
builder.set_app_path(&current_exe.canonicalize()?.display().to_string());
#[cfg(target_os = "linux")]
if let Some(appimage) = app
.env()
.appimage
.and_then(|p| p.to_str().map(|s| s.to_string()))
{
builder.set_app_path(&appimage);
} else {
builder.set_app_path(&current_exe.display().to_string());
}
app.manage(AutoLaunchManager(
builder.build().map_err(|e| e.to_string())?,
));
Ok(())
})
.build()
}
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "tauri-plugin-fs-extra"
version = "0.1.0"
edition.workspace = true
authors.workspace = true
license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde.workspace = true
serde_json.workspace = true
tauri.workspace = true
log.workspace = true
thiserror.workspace = true
+24
View File
@@ -0,0 +1,24 @@
var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumerable:!0});};
var f={};e(f,{convertFileSrc:()=>w,invoke:()=>c,transformCallback:()=>s});function u(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function s(e,r=!1){let n=u(),t=`_${n}`;return Object.defineProperty(window,t,{value:o=>(r&&Reflect.deleteProperty(window,t),e==null?void 0:e(o)),writable:!1,configurable:!0}),n}async function c(e,r={}){return new Promise((n,t)=>{let o=s(i=>{n(i),Reflect.deleteProperty(window,`_${a}`);},!0),a=s(i=>{t(i),Reflect.deleteProperty(window,`_${o}`);},!0);window.__TAURI_IPC__({cmd:e,callback:o,error:a,...r});})}function w(e,r="asset"){let n=encodeURIComponent(e);return navigator.userAgent.includes("Windows")?`https://${r}.localhost/${n}`:`${r}://localhost/${n}`}
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
async function metadata(path) {
return await c("plugin:fs-extra|metadata", {
path,
}).then((metadata) => {
const { accessedAtMs, createdAtMs, modifiedAtMs, ...data } = metadata;
return {
accessedAt: new Date(accessedAtMs),
createdAt: new Date(createdAtMs),
modifiedAt: new Date(modifiedAtMs),
...data,
};
});
}
async function exists(path) {
return await c("plugin:fs-extra|exists", { path });
}
export { exists, metadata };
//# sourceMappingURL=index.min.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.min.js","sources":["../../../../node_modules/.pnpm/@tauri-apps+api@1.2.0/node_modules/@tauri-apps/api/chunk-FEIY7W7S.js","../../../../node_modules/.pnpm/@tauri-apps+api@1.2.0/node_modules/@tauri-apps/api/chunk-RCPA6UVN.js","../index.ts"],"sourcesContent":["var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumerable:!0})};export{e as a};\n","import{a as d}from\"./chunk-FEIY7W7S.js\";var f={};d(f,{convertFileSrc:()=>w,invoke:()=>c,transformCallback:()=>s});function u(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function s(e,r=!1){let n=u(),t=`_${n}`;return Object.defineProperty(window,t,{value:o=>(r&&Reflect.deleteProperty(window,t),e==null?void 0:e(o)),writable:!1,configurable:!0}),n}async function c(e,r={}){return new Promise((n,t)=>{let o=s(i=>{n(i),Reflect.deleteProperty(window,`_${a}`)},!0),a=s(i=>{t(i),Reflect.deleteProperty(window,`_${o}`)},!0);window.__TAURI_IPC__({cmd:e,callback:o,error:a,...r})})}function w(e,r=\"asset\"){let n=encodeURIComponent(e);return navigator.userAgent.includes(\"Windows\")?`https://${r}.localhost/${n}`:`${r}://localhost/${n}`}export{s as a,c as b,w as c,f as d};\n",null],"names":["d","invoke"],"mappings":"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"}
+22
View File
@@ -0,0 +1,22 @@
import { invoke } from '@tauri-apps/api/tauri';
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
async function metadata(path) {
return await invoke("plugin:fs-extra|metadata", {
path,
}).then((metadata) => {
const { accessedAtMs, createdAtMs, modifiedAtMs, ...data } = metadata;
return {
accessedAt: new Date(accessedAtMs),
createdAt: new Date(createdAtMs),
modifiedAt: new Date(modifiedAtMs),
...data,
};
});
}
async function exists(path) {
return await invoke("plugin:fs-extra|exists", { path });
}
export { exists, metadata };
//# sourceMappingURL=index.mjs.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.mjs","sources":["../index.ts"],"sourcesContent":[null],"names":[],"mappings":";;AAAA;AAiHO,eAAe,QAAQ,CAAC,IAAY,EAAA;AACzC,IAAA,OAAO,MAAM,MAAM,CAAkB,0BAA0B,EAAE;QAC/D,IAAI;AACL,KAAA,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,KAAI;AACnB,QAAA,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,GAAG,IAAI,EAAE,GAAG,QAAQ,CAAC;QACtE,OAAO;AACL,YAAA,UAAU,EAAE,IAAI,IAAI,CAAC,YAAY,CAAC;AAClC,YAAA,SAAS,EAAE,IAAI,IAAI,CAAC,WAAW,CAAC;AAChC,YAAA,UAAU,EAAE,IAAI,IAAI,CAAC,YAAY,CAAC;AAClC,YAAA,GAAG,IAAI;SACR,CAAC;AACJ,KAAC,CAAC,CAAC;AACL,CAAC;AAEM,eAAe,MAAM,CAAC,IAAY,EAAA;IACvC,OAAO,MAAM,MAAM,CAAC,wBAAwB,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;AAC1D;;;;"}
+130
View File
@@ -0,0 +1,130 @@
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import { invoke } from "@tauri-apps/api/tauri";
export interface Permissions {
/**
* `true` if these permissions describe a readonly (unwritable) file.
*/
readonly: boolean;
/**
* The underlying raw `st_mode` bits that contain the standard Unix permissions for this file.
*/
mode: number | undefined;
}
/**
* Metadata information about a file.
* This structure is returned from the `metadata` function or method
* and represents known metadata about a file such as its permissions, size, modification times, etc.
*/
export interface Metadata {
/**
* The last access time of this metadata.
*/
accessedAt: Date;
/**
* The creation time listed in this metadata.
*/
createdAt: Date;
/**
* The last modification time listed in this metadata.
*/
modifiedAt: Date;
/**
* `true` if this metadata is for a directory.
*/
isDir: boolean;
/**
* `true` if this metadata is for a regular file.
*/
isFile: boolean;
/**
* `true` if this metadata is for a symbolic link.
*/
isSymlink: boolean;
/**
* The size of the file, in bytes, this metadata is for.
*/
size: number;
/**
* The permissions of the file this metadata is for.
*/
permissions: Permissions;
/**
* The ID of the device containing the file. Only available on Unix.
*/
dev: number | undefined;
/**
* The inode number. Only available on Unix.
*/
ino: number | undefined;
/**
* The rights applied to this file. Only available on Unix.
*/
mode: number | undefined;
/**
* The number of hard links pointing to this file. Only available on Unix.
*/
nlink: number | undefined;
/**
* The user ID of the owner of this file. Only available on Unix.
*/
uid: number | undefined;
/**
* The group ID of the owner of this file. Only available on Unix.
*/
gid: number | undefined;
/**
* The device ID of this file (if it is a special one). Only available on Unix.
*/
rdev: number | undefined;
/**
* The block size for filesystem I/O. Only available on Unix.
*/
blksize: number | undefined;
/**
* The number of blocks allocated to the file, in 512-byte units. Only available on Unix.
*/
blocks: number | undefined;
}
interface BackendMetadata {
accessedAtMs: number;
createdAtMs: number;
modifiedAtMs: number;
isDir: boolean;
isFile: boolean;
isSymlink: boolean;
size: number;
permissions: Permissions;
dev: number | undefined;
ino: number | undefined;
mode: number | undefined;
nlink: number | undefined;
uid: number | undefined;
gid: number | undefined;
rdev: number | undefined;
blksize: number | undefined;
blocks: number | undefined;
}
export async function metadata(path: string): Promise<Metadata> {
return await invoke<BackendMetadata>("plugin:fs-extra|metadata", {
path,
}).then((metadata) => {
const { accessedAtMs, createdAtMs, modifiedAtMs, ...data } = metadata;
return {
accessedAt: new Date(accessedAtMs),
createdAt: new Date(createdAtMs),
modifiedAt: new Date(modifiedAtMs),
...data,
};
});
}
export async function exists(path: string): Promise<boolean> {
return await invoke("plugin:fs-extra|exists", { path });
}
+29
View File
@@ -0,0 +1,29 @@
{
"name": "tauri-plugin-fs-extra-api",
"version": "0.0.0",
"license": "MIT or APACHE-2.0",
"type": "module",
"browser": "dist/index.min.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"browser": "./dist/index.min.js"
},
"scripts": {
"build": "rollup -c"
},
"files": [
"dist",
"!dist/**/*.map",
"README.md",
"LICENSE"
],
"devDependencies": {
"tslib": "^2.4.1"
},
"dependencies": {
"@tauri-apps/api": "^1.2.0"
}
}
@@ -0,0 +1,10 @@
import { readFileSync } from "fs";
import { createConfig } from "../../../shared/rollup.config.mjs";
export default createConfig({
pkg: JSON.parse(
readFileSync(new URL("./package.json", import.meta.url), "utf8")
),
external: [/^@tauri-apps\/api/],
});
+1
View File
@@ -0,0 +1 @@
../../../shared/tsconfig.json
+145
View File
@@ -0,0 +1,145 @@
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::{ser::Serializer, Serialize};
use tauri::{command, plugin::Plugin, Invoke, Runtime};
use std::{
path::PathBuf,
time::{SystemTime, UNIX_EPOCH},
};
#[cfg(unix)]
use std::os::unix::fs::{MetadataExt, PermissionsExt};
#[cfg(windows)]
use std::os::windows::fs::MetadataExt;
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Permissions {
readonly: bool,
#[cfg(unix)]
mode: u32,
}
#[cfg(unix)]
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct UnixMetadata {
dev: u64,
ino: u64,
mode: u32,
nlink: u64,
uid: u32,
gid: u32,
rdev: u64,
blksize: u64,
blocks: u64,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Metadata {
accessed_at_ms: u64,
created_at_ms: u64,
modified_at_ms: u64,
is_dir: bool,
is_file: bool,
is_symlink: bool,
size: u64,
permissions: Permissions,
#[cfg(unix)]
#[serde(flatten)]
unix: UnixMetadata,
#[cfg(windows)]
file_attributes: u32,
}
fn system_time_to_ms(time: std::io::Result<SystemTime>) -> u64 {
time.map(|t| {
let duration_since_epoch = t.duration_since(UNIX_EPOCH).unwrap();
duration_since_epoch.as_millis() as u64
})
.unwrap_or_default()
}
#[command]
async fn metadata(path: PathBuf) -> Result<Metadata> {
let metadata = std::fs::metadata(path)?;
let file_type = metadata.file_type();
let permissions = metadata.permissions();
Ok(Metadata {
accessed_at_ms: system_time_to_ms(metadata.accessed()),
created_at_ms: system_time_to_ms(metadata.created()),
modified_at_ms: system_time_to_ms(metadata.modified()),
is_dir: file_type.is_dir(),
is_file: file_type.is_file(),
is_symlink: file_type.is_symlink(),
size: metadata.len(),
permissions: Permissions {
readonly: permissions.readonly(),
#[cfg(unix)]
mode: permissions.mode(),
},
#[cfg(unix)]
unix: UnixMetadata {
dev: metadata.dev(),
ino: metadata.ino(),
mode: metadata.mode(),
nlink: metadata.nlink(),
uid: metadata.uid(),
gid: metadata.gid(),
rdev: metadata.rdev(),
blksize: metadata.blksize(),
blocks: metadata.blocks(),
},
#[cfg(windows)]
file_attributes: metadata.file_attributes(),
})
}
#[command]
async fn exists(path: PathBuf) -> bool {
path.exists()
}
/// Tauri plugin.
pub struct FsExtra<R: Runtime> {
invoke_handler: Box<dyn Fn(Invoke<R>) + Send + Sync>,
}
impl<R: Runtime> Default for FsExtra<R> {
fn default() -> Self {
Self {
invoke_handler: Box::new(tauri::generate_handler![exists, metadata]),
}
}
}
impl<R: Runtime> Plugin<R> for FsExtra<R> {
fn name(&self) -> &'static str {
"fs-extra"
}
fn extend_api(&mut self, message: Invoke<R>) {
(self.invoke_handler)(message)
}
}
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "tauri-plugin-fs-watch"
version = "0.1.0"
edition.workspace = true
authors.workspace = true
license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde.workspace = true
serde_json.workspace = true
tauri.workspace = true
log.workspace = true
thiserror.workspace = true
notify = "4.0"
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+64
View File
@@ -0,0 +1,64 @@
import { invoke } from '@tauri-apps/api/tauri';
import { appWindow } from '@tauri-apps/api/window';
const w = appWindow;
async function unwatch(id) {
await invoke("plugin:fs-watch|unwatch", { id });
}
async function watch(paths, options, cb) {
const opts = {
recursive: false,
delayMs: 2000,
...options,
};
let watchPaths;
if (typeof paths === "string") {
watchPaths = [paths];
}
else {
watchPaths = paths;
}
const id = window.crypto.getRandomValues(new Uint32Array(1))[0];
await invoke("plugin:fs-watch|watch", {
id,
paths: watchPaths,
options: opts,
});
const unlisten = await w.listen(`watcher://debounced-event/${id}`, (event) => {
cb(event.payload);
});
return () => {
void unwatch(id);
unlisten();
};
}
async function watchImmediate(paths, options, cb) {
const opts = {
recursive: false,
...options,
delayMs: null,
};
let watchPaths;
if (typeof paths === "string") {
watchPaths = [paths];
}
else {
watchPaths = paths;
}
const id = window.crypto.getRandomValues(new Uint32Array(1))[0];
await invoke("plugin:fs-watch|watch", {
id,
paths: watchPaths,
options: opts,
});
const unlisten = await w.listen(`watcher://raw-event/${id}`, (event) => {
cb(event.payload);
});
return () => {
void unwatch(id);
unlisten();
};
}
export { watch, watchImmediate };
//# sourceMappingURL=index.mjs.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.mjs","sources":["../index.ts"],"sourcesContent":[null],"names":[],"mappings":";;;AAIA,MAAM,CAAC,GAAkB,SAAS,CAAC;AA2BnC,eAAe,OAAO,CAAC,EAAU,EAAA;IAC/B,MAAM,MAAM,CAAC,yBAAyB,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;AAClD,CAAC;AAEM,eAAe,KAAK,CACzB,KAAwB,EACxB,OAA8B,EAC9B,EAAmC,EAAA;AAEnC,IAAA,MAAM,IAAI,GAAG;AACX,QAAA,SAAS,EAAE,KAAK;AAChB,QAAA,OAAO,EAAE,IAAI;AACb,QAAA,GAAG,OAAO;KACX,CAAC;AACF,IAAA,IAAI,UAAU,CAAC;AACf,IAAA,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE;AAC7B,QAAA,UAAU,GAAG,CAAC,KAAK,CAAC,CAAC;AACtB,KAAA;AAAM,SAAA;QACL,UAAU,GAAG,KAAK,CAAC;AACpB,KAAA;AAED,IAAA,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAEhE,MAAM,MAAM,CAAC,uBAAuB,EAAE;QACpC,EAAE;AACF,QAAA,KAAK,EAAE,UAAU;AACjB,QAAA,OAAO,EAAE,IAAI;AACd,KAAA,CAAC,CAAC;AAEH,IAAA,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,MAAM,CAC7B,CAA6B,0BAAA,EAAA,EAAE,CAAE,CAAA,EACjC,CAAC,KAAK,KAAI;AACR,QAAA,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;AACpB,KAAC,CACF,CAAC;AAEF,IAAA,OAAO,MAAK;AACV,QAAA,KAAK,OAAO,CAAC,EAAE,CAAC,CAAC;AACjB,QAAA,QAAQ,EAAE,CAAC;AACb,KAAC,CAAC;AACJ,CAAC;AAEM,eAAe,cAAc,CAClC,KAAwB,EACxB,OAAqB,EACrB,EAA6B,EAAA;AAE7B,IAAA,MAAM,IAAI,GAAG;AACX,QAAA,SAAS,EAAE,KAAK;AAChB,QAAA,GAAG,OAAO;AACV,QAAA,OAAO,EAAE,IAAI;KACd,CAAC;AACF,IAAA,IAAI,UAAU,CAAC;AACf,IAAA,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE;AAC7B,QAAA,UAAU,GAAG,CAAC,KAAK,CAAC,CAAC;AACtB,KAAA;AAAM,SAAA;QACL,UAAU,GAAG,KAAK,CAAC;AACpB,KAAA;AAED,IAAA,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAEhE,MAAM,MAAM,CAAC,uBAAuB,EAAE;QACpC,EAAE;AACF,QAAA,KAAK,EAAE,UAAU;AACjB,QAAA,OAAO,EAAE,IAAI;AACd,KAAA,CAAC,CAAC;AAEH,IAAA,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,MAAM,CAC7B,CAAuB,oBAAA,EAAA,EAAE,CAAE,CAAA,EAC3B,CAAC,KAAK,KAAI;AACR,QAAA,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;AACpB,KAAC,CACF,CAAC;AAEF,IAAA,OAAO,MAAK;AACV,QAAA,KAAK,OAAO,CAAC,EAAE,CAAC,CAAC;AACjB,QAAA,QAAQ,EAAE,CAAC;AACb,KAAC,CAAC;AACJ;;;;"}
+110
View File
@@ -0,0 +1,110 @@
import { invoke } from "@tauri-apps/api/tauri";
import { UnlistenFn } from "@tauri-apps/api/event";
import { appWindow, WebviewWindow } from "@tauri-apps/api/window";
const w: WebviewWindow = appWindow;
export interface WatchOptions {
recursive?: boolean;
}
export interface DebouncedWatchOptions extends WatchOptions {
delayMs?: number;
}
export interface RawEvent {
path: string | null;
operation: number;
cookie: number | null;
}
export type DebouncedEvent =
| { type: "NoticeWrite"; payload: string }
| { type: "NoticeRemove"; payload: string }
| { type: "Create"; payload: string }
| { type: "Write"; payload: string }
| { type: "Chmod"; payload: string }
| { type: "Remove"; payload: string }
| { type: "Rename"; payload: string }
| { type: "Rescan"; payload: null }
| { type: "Error"; payload: { error: string; path: string | null } };
async function unwatch(id: number): Promise<void> {
await invoke("plugin:fs-watch|unwatch", { id });
}
export async function watch(
paths: string | string[],
options: DebouncedWatchOptions,
cb: (event: DebouncedEvent) => void
): Promise<UnlistenFn> {
const opts = {
recursive: false,
delayMs: 2000,
...options,
};
let watchPaths;
if (typeof paths === "string") {
watchPaths = [paths];
} else {
watchPaths = paths;
}
const id = window.crypto.getRandomValues(new Uint32Array(1))[0];
await invoke("plugin:fs-watch|watch", {
id,
paths: watchPaths,
options: opts,
});
const unlisten = await w.listen<DebouncedEvent>(
`watcher://debounced-event/${id}`,
(event) => {
cb(event.payload);
}
);
return () => {
void unwatch(id);
unlisten();
};
}
export async function watchImmediate(
paths: string | string[],
options: WatchOptions,
cb: (event: RawEvent) => void
): Promise<UnlistenFn> {
const opts = {
recursive: false,
...options,
delayMs: null,
};
let watchPaths;
if (typeof paths === "string") {
watchPaths = [paths];
} else {
watchPaths = paths;
}
const id = window.crypto.getRandomValues(new Uint32Array(1))[0];
await invoke("plugin:fs-watch|watch", {
id,
paths: watchPaths,
options: opts,
});
const unlisten = await w.listen<RawEvent>(
`watcher://raw-event/${id}`,
(event) => {
cb(event.payload);
}
);
return () => {
void unwatch(id);
unlisten();
};
}
+29
View File
@@ -0,0 +1,29 @@
{
"name": "tauri-plugin-fs-watch-api",
"version": "0.0.0",
"license": "MIT or APACHE-2.0",
"type": "module",
"browser": "dist/index.min.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"browser": "./dist/index.min.js"
},
"scripts": {
"build": "rollup -c"
},
"files": [
"dist",
"!dist/**/*.map",
"README.md",
"LICENSE"
],
"devDependencies": {
"tslib": "^2.4.1"
},
"dependencies": {
"@tauri-apps/api": "^1.2.0"
}
}
@@ -0,0 +1,10 @@
import { readFileSync } from "fs";
import { createConfig } from "../../../shared/rollup.config.mjs";
export default createConfig({
pkg: JSON.parse(
readFileSync(new URL("./package.json", import.meta.url), "utf8")
),
external: [/^@tauri-apps\/api/],
});
+1
View File
@@ -0,0 +1 @@
../../../shared/tsconfig.json
+189
View File
@@ -0,0 +1,189 @@
use notify::{
raw_watcher, watcher, DebouncedEvent, Op, RawEvent, RecommendedWatcher, RecursiveMode,
Watcher as _,
};
use serde::{ser::Serializer, Deserialize, Serialize};
use serde_json::Value as JsonValue;
use tauri::{command, plugin::Plugin, AppHandle, Invoke, Manager, Runtime, State, Window};
use std::{
collections::HashMap,
path::PathBuf,
sync::{
mpsc::{channel, Receiver},
Mutex,
},
thread::spawn,
time::Duration,
};
type Result<T> = std::result::Result<T, Error>;
type Id = u32;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Watch(#[from] notify::Error),
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
#[derive(Default)]
struct WatcherCollection(Mutex<HashMap<Id, (RecommendedWatcher, Vec<PathBuf>)>>);
#[derive(Clone, Serialize)]
struct RawEventWrapper {
path: Option<PathBuf>,
operation: u32,
cookie: Option<u32>,
}
#[derive(Clone, Serialize)]
#[serde(tag = "type", content = "payload")]
enum DebouncedEventWrapper {
NoticeWrite(PathBuf),
NoticeRemove(PathBuf),
Create(PathBuf),
Write(PathBuf),
Chmod(PathBuf),
Remove(PathBuf),
Rename(PathBuf, PathBuf),
Rescan,
Error {
error: String,
path: Option<PathBuf>,
},
}
impl From<DebouncedEvent> for DebouncedEventWrapper {
fn from(event: DebouncedEvent) -> Self {
match event {
DebouncedEvent::NoticeWrite(path) => Self::NoticeWrite(path),
DebouncedEvent::NoticeRemove(path) => Self::NoticeRemove(path),
DebouncedEvent::Create(path) => Self::Create(path),
DebouncedEvent::Write(path) => Self::Write(path),
DebouncedEvent::Chmod(path) => Self::Chmod(path),
DebouncedEvent::Remove(path) => Self::Remove(path),
DebouncedEvent::Rename(from, to) => Self::Rename(from, to),
DebouncedEvent::Rescan => Self::Rescan,
DebouncedEvent::Error(error, path) => Self::Error {
error: error.to_string(),
path,
},
}
}
}
fn watch_raw<R: Runtime>(window: Window<R>, rx: Receiver<RawEvent>, id: Id) {
spawn(move || {
let event_name = format!("watcher://raw-event/{}", id);
while let Ok(event) = rx.recv() {
let _ = window.emit(
&event_name,
RawEventWrapper {
path: event.path,
operation: event.op.unwrap_or_else(|_| Op::empty()).bits(),
cookie: event.cookie,
},
);
}
});
}
fn watch_debounced<R: Runtime>(window: Window<R>, rx: Receiver<DebouncedEvent>, id: Id) {
spawn(move || {
let event_name = format!("watcher://debounced-event/{}", id);
while let Ok(event) = rx.recv() {
let _ = window.emit(&event_name, DebouncedEventWrapper::from(event));
}
});
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct WatchOptions {
delay_ms: Option<u64>,
recursive: bool,
}
#[command]
async fn watch<R: Runtime>(
window: Window<R>,
watchers: State<'_, WatcherCollection>,
id: Id,
paths: Vec<PathBuf>,
options: WatchOptions,
) -> Result<()> {
let mode = if options.recursive {
RecursiveMode::Recursive
} else {
RecursiveMode::NonRecursive
};
let watcher = if let Some(delay) = options.delay_ms {
let (tx, rx) = channel();
let mut watcher = watcher(tx, Duration::from_millis(delay))?;
for path in &paths {
watcher.watch(path, mode)?;
}
watch_debounced(window, rx, id);
watcher
} else {
let (tx, rx) = channel();
let mut watcher = raw_watcher(tx)?;
for path in &paths {
watcher.watch(path, mode)?;
}
watch_raw(window, rx, id);
watcher
};
watchers.0.lock().unwrap().insert(id, (watcher, paths));
Ok(())
}
#[command]
async fn unwatch(watchers: State<'_, WatcherCollection>, id: Id) -> Result<()> {
if let Some((mut watcher, paths)) = watchers.0.lock().unwrap().remove(&id) {
for path in paths {
watcher.unwatch(path)?;
}
}
Ok(())
}
/// Tauri plugin.
pub struct Watcher<R: Runtime> {
invoke_handler: Box<dyn Fn(Invoke<R>) + Send + Sync>,
}
impl<R: Runtime> Default for Watcher<R> {
fn default() -> Self {
Self {
invoke_handler: Box::new(tauri::generate_handler![watch, unwatch]),
}
}
}
impl<R: Runtime> Plugin<R> for Watcher<R> {
fn name(&self) -> &'static str {
"fs-watch"
}
fn initialize(&mut self, app: &AppHandle<R>, _config: JsonValue) -> tauri::plugin::Result<()> {
app.manage(WatcherCollection::default());
Ok(())
}
fn extend_api(&mut self, message: Invoke<R>) {
(self.invoke_handler)(message)
}
}
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "tauri-plugin-localhost"
version = "0.1.0"
edition.workspace = true
authors.workspace = true
license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde.workspace = true
serde_json.workspace = true
tauri.workspace = true
log.workspace = true
thiserror.workspace = true
tiny_http = "0.11"
http = "0.2"
+118
View File
@@ -0,0 +1,118 @@
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::collections::HashMap;
use http::Uri;
use tauri::{
plugin::{Builder as PluginBuilder, TauriPlugin},
Runtime,
};
use tiny_http::{Header, Response as HttpResponse, Server};
pub struct Request {
url: String,
}
impl Request {
pub fn url(&self) -> &str {
&self.url
}
}
pub struct Response {
headers: HashMap<String, String>,
}
impl Response {
pub fn add_header<H: Into<String>, V: Into<String>>(&mut self, header: H, value: V) {
self.headers.insert(header.into(), value.into());
}
}
type OnRequest = Option<Box<dyn Fn(&Request, &mut Response) + Send + Sync>>;
pub struct Builder {
port: u16,
on_request: OnRequest,
}
impl Builder {
pub fn new(port: u16) -> Self {
Self {
port,
on_request: None,
}
}
pub fn on_request<F: Fn(&Request, &mut Response) + Send + Sync + 'static>(
mut self,
f: F,
) -> Self {
self.on_request.replace(Box::new(f));
self
}
pub fn build<R: Runtime>(mut self) -> TauriPlugin<R> {
let port = self.port;
let on_request = self.on_request.take();
PluginBuilder::new("localhost")
.setup(move |app| {
let asset_resolver = app.asset_resolver();
std::thread::spawn(move || {
let server = Server::http(&format!("localhost:{}", port))
.expect("Unable to spawn server");
for req in server.incoming_requests() {
let path = req
.url()
.parse::<Uri>()
.map(|uri| uri.path().into())
.unwrap_or_else(|_| req.url().into());
#[allow(unused_mut)]
if let Some(mut asset) = asset_resolver.get(path) {
let request = Request {
url: req.url().into(),
};
let mut response = Response {
headers: Default::default(),
};
response.add_header("Content-Type", asset.mime_type);
if let Some(csp) = asset.csp_header {
response
.headers
.insert("Content-Security-Policy".into(), csp);
}
if let Some(on_request) = &on_request {
on_request(&request, &mut response);
}
#[cfg(target_os = "linux")]
if let Some(response_csp) =
response.headers.get("Content-Security-Policy")
{
let html = String::from_utf8_lossy(&asset.bytes);
let body =
html.replacen(tauri::utils::html::CSP_TOKEN, response_csp, 1);
asset.bytes = body.as_bytes().to_vec();
}
let mut resp = HttpResponse::from_data(asset.bytes);
for (header, value) in response.headers {
if let Ok(h) = Header::from_bytes(header.as_bytes(), value) {
resp.add_header(h);
}
}
req.respond(resp).expect("unable to setup response");
}
}
});
Ok(())
})
.build()
}
}
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "log"
version = "0.1.0"
edition.workspace = true
authors.workspace = true
license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde.workspace = true
serde_json.workspace = true
tauri.workspace = true
serde_repr = "0.1"
byte-unit = "4.0"
fern = "0.6"
log = { workspace = true, features = ["kv_unstable"] }
time = { version = "0.3", features = ["formatting"] }
+176
View File
@@ -0,0 +1,176 @@
var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumerable:!0});};
var f={};e(f,{convertFileSrc:()=>w,invoke:()=>c$1,transformCallback:()=>s$1});function u$1(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function s$1(e,r=!1){let n=u$1(),t=`_${n}`;return Object.defineProperty(window,t,{value:o=>(r&&Reflect.deleteProperty(window,t),e==null?void 0:e(o)),writable:!1,configurable:!0}),n}async function c$1(e,r={}){return new Promise((n,t)=>{let o=s$1(i=>{n(i),Reflect.deleteProperty(window,`_${a}`);},!0),a=s$1(i=>{t(i),Reflect.deleteProperty(window,`_${o}`);},!0);window.__TAURI_IPC__({cmd:e,callback:o,error:a,...r});})}function w(e,r="asset"){let n=encodeURIComponent(e);return navigator.userAgent.includes("Windows")?`https://${r}.localhost/${n}`:`${r}://localhost/${n}`}
async function a(i){return c$1("tauri",i)}
var W={};e(W,{TauriEvent:()=>c,emit:()=>D,listen:()=>E,once:()=>_});async function s(n,t){return a({__tauriModule:"Event",message:{cmd:"unlisten",event:n,eventId:t}})}async function m(n,t,i){await a({__tauriModule:"Event",message:{cmd:"emit",event:n,windowLabel:t,payload:i}});}async function o(n,t,i){return a({__tauriModule:"Event",message:{cmd:"listen",event:n,windowLabel:t,handler:s$1(i)}}).then(r=>async()=>s(n,r))}async function u(n,t,i){return o(n,t,r=>{i(r),s(n,r.id).catch(()=>{});})}var c=(e=>(e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_CREATED="tauri://window-created",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_FILE_DROP="tauri://file-drop",e.WINDOW_FILE_DROP_HOVER="tauri://file-drop-hover",e.WINDOW_FILE_DROP_CANCELLED="tauri://file-drop-cancelled",e.MENU="tauri://menu",e.CHECK_UPDATE="tauri://update",e.UPDATE_AVAILABLE="tauri://update-available",e.INSTALL_UPDATE="tauri://update-install",e.STATUS_UPDATE="tauri://update-status",e.DOWNLOAD_PROGRESS="tauri://update-download-progress",e))(c||{});async function E(n,t){return o(n,null,t)}async function _(n,t){return u(n,null,t)}async function D(n,t){return m(n,void 0,t)}
var LogLevel;
(function (LogLevel) {
/**
* The "trace" level.
*
* Designates very low priority, often extremely verbose, information.
*/
LogLevel[LogLevel["Trace"] = 1] = "Trace";
/**
* The "debug" level.
*
* Designates lower priority information.
*/
LogLevel[LogLevel["Debug"] = 2] = "Debug";
/**
* The "info" level.
*
* Designates useful information.
*/
LogLevel[LogLevel["Info"] = 3] = "Info";
/**
* The "warn" level.
*
* Designates hazardous situations.
*/
LogLevel[LogLevel["Warn"] = 4] = "Warn";
/**
* The "error" level.
*
* Designates very serious errors.
*/
LogLevel[LogLevel["Error"] = 5] = "Error";
})(LogLevel || (LogLevel = {}));
async function log(level, message, options) {
var _a, _b;
const traces = (_a = new Error().stack) === null || _a === void 0 ? void 0 : _a.split("\n").map((line) => line.split("@"));
const filtered = traces === null || traces === void 0 ? void 0 : traces.filter(([name, location]) => {
return name.length > 0 && location !== "[native code]";
});
const { file, line, ...keyValues } = options !== null && options !== void 0 ? options : {};
await c$1("plugin:log|log", {
level,
message,
location: (_b = filtered === null || filtered === void 0 ? void 0 : filtered[0]) === null || _b === void 0 ? void 0 : _b.filter((v) => v.length > 0).join("@"),
file,
line,
keyValues,
});
}
/**
* Logs a message at the error level.
*
* @param message
*
* # Examples
*
* ```js
* import { error } from 'tauri-plugin-log-api';
*
* const err_info = "No connection";
* const port = 22;
*
* error(`Error: ${err_info} on port ${port}`);
* ```
*/
async function error(message, options) {
await log(LogLevel.Error, message, options);
}
/**
* Logs a message at the warn level.
*
* @param message
*
* # Examples
*
* ```js
* import { warn } from 'tauri-plugin-log-api';
*
* const warn_description = "Invalid Input";
*
* warn(`Warning! {warn_description}!`);
* ```
*/
async function warn(message, options) {
await log(LogLevel.Warn, message, options);
}
/**
* Logs a message at the info level.
*
* @param message
*
* # Examples
*
* ```js
* import { info } from 'tauri-plugin-log-api';
*
* const conn_info = { port: 40, speed: 3.20 };
*
* info(`Connected to port {conn_info.port} at {conn_info.speed} Mb/s`);
* ```
*/
async function info(message, options) {
await log(LogLevel.Info, message, options);
}
/**
* Logs a message at the debug level.
*
* @param message
*
* # Examples
*
* ```js
* import { debug } from 'tauri-plugin-log-api';
*
* const pos = { x: 3.234, y: -1.223 };
*
* debug(`New position: x: {pos.x}, y: {pos.y}`);
* ```
*/
async function debug(message, options) {
await log(LogLevel.Debug, message, options);
}
/**
* Logs a message at the trace level.
*
* @param message
*
* # Examples
*
* ```js
* import { trace } from 'tauri-plugin-log-api';
*
* let pos = { x: 3.234, y: -1.223 };
*
* trace(`Position is: x: {pos.x}, y: {pos.y}`);
* ```
*/
async function trace(message, options) {
await log(LogLevel.Trace, message, options);
}
async function attachConsole() {
return await E("log://log", (event) => {
const payload = event.payload;
switch (payload.level) {
case LogLevel.Trace:
console.log(payload.message);
break;
case LogLevel.Debug:
console.debug(payload.message);
break;
case LogLevel.Info:
console.info(payload.message);
break;
case LogLevel.Warn:
console.warn(payload.message);
break;
case LogLevel.Error:
console.error(payload.message);
break;
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`unknown log level ${payload.level}`);
}
});
}
export { attachConsole, debug, error, info, trace, warn };
//# sourceMappingURL=index.min.js.map
File diff suppressed because one or more lines are too long
+171
View File
@@ -0,0 +1,171 @@
import { invoke } from '@tauri-apps/api/tauri';
import { listen } from '@tauri-apps/api/event';
var LogLevel;
(function (LogLevel) {
/**
* The "trace" level.
*
* Designates very low priority, often extremely verbose, information.
*/
LogLevel[LogLevel["Trace"] = 1] = "Trace";
/**
* The "debug" level.
*
* Designates lower priority information.
*/
LogLevel[LogLevel["Debug"] = 2] = "Debug";
/**
* The "info" level.
*
* Designates useful information.
*/
LogLevel[LogLevel["Info"] = 3] = "Info";
/**
* The "warn" level.
*
* Designates hazardous situations.
*/
LogLevel[LogLevel["Warn"] = 4] = "Warn";
/**
* The "error" level.
*
* Designates very serious errors.
*/
LogLevel[LogLevel["Error"] = 5] = "Error";
})(LogLevel || (LogLevel = {}));
async function log(level, message, options) {
var _a, _b;
const traces = (_a = new Error().stack) === null || _a === void 0 ? void 0 : _a.split("\n").map((line) => line.split("@"));
const filtered = traces === null || traces === void 0 ? void 0 : traces.filter(([name, location]) => {
return name.length > 0 && location !== "[native code]";
});
const { file, line, ...keyValues } = options !== null && options !== void 0 ? options : {};
await invoke("plugin:log|log", {
level,
message,
location: (_b = filtered === null || filtered === void 0 ? void 0 : filtered[0]) === null || _b === void 0 ? void 0 : _b.filter((v) => v.length > 0).join("@"),
file,
line,
keyValues,
});
}
/**
* Logs a message at the error level.
*
* @param message
*
* # Examples
*
* ```js
* import { error } from 'tauri-plugin-log-api';
*
* const err_info = "No connection";
* const port = 22;
*
* error(`Error: ${err_info} on port ${port}`);
* ```
*/
async function error(message, options) {
await log(LogLevel.Error, message, options);
}
/**
* Logs a message at the warn level.
*
* @param message
*
* # Examples
*
* ```js
* import { warn } from 'tauri-plugin-log-api';
*
* const warn_description = "Invalid Input";
*
* warn(`Warning! {warn_description}!`);
* ```
*/
async function warn(message, options) {
await log(LogLevel.Warn, message, options);
}
/**
* Logs a message at the info level.
*
* @param message
*
* # Examples
*
* ```js
* import { info } from 'tauri-plugin-log-api';
*
* const conn_info = { port: 40, speed: 3.20 };
*
* info(`Connected to port {conn_info.port} at {conn_info.speed} Mb/s`);
* ```
*/
async function info(message, options) {
await log(LogLevel.Info, message, options);
}
/**
* Logs a message at the debug level.
*
* @param message
*
* # Examples
*
* ```js
* import { debug } from 'tauri-plugin-log-api';
*
* const pos = { x: 3.234, y: -1.223 };
*
* debug(`New position: x: {pos.x}, y: {pos.y}`);
* ```
*/
async function debug(message, options) {
await log(LogLevel.Debug, message, options);
}
/**
* Logs a message at the trace level.
*
* @param message
*
* # Examples
*
* ```js
* import { trace } from 'tauri-plugin-log-api';
*
* let pos = { x: 3.234, y: -1.223 };
*
* trace(`Position is: x: {pos.x}, y: {pos.y}`);
* ```
*/
async function trace(message, options) {
await log(LogLevel.Trace, message, options);
}
async function attachConsole() {
return await listen("log://log", (event) => {
const payload = event.payload;
switch (payload.level) {
case LogLevel.Trace:
console.log(payload.message);
break;
case LogLevel.Debug:
console.debug(payload.message);
break;
case LogLevel.Info:
console.info(payload.message);
break;
case LogLevel.Warn:
console.warn(payload.message);
break;
case LogLevel.Error:
console.error(payload.message);
break;
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`unknown log level ${payload.level}`);
}
});
}
export { attachConsole, debug, error, info, trace, warn };
//# sourceMappingURL=index.mjs.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.mjs","sources":["../index.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
+206
View File
@@ -0,0 +1,206 @@
import { invoke } from "@tauri-apps/api/tauri";
import { listen, UnlistenFn } from "@tauri-apps/api/event";
export type LogOptions = {
file?: string;
line?: number;
} & Record<string, string | undefined>;
enum LogLevel {
/**
* The "trace" level.
*
* Designates very low priority, often extremely verbose, information.
*/
Trace = 1,
/**
* The "debug" level.
*
* Designates lower priority information.
*/
Debug,
/**
* The "info" level.
*
* Designates useful information.
*/
Info,
/**
* The "warn" level.
*
* Designates hazardous situations.
*/
Warn,
/**
* The "error" level.
*
* Designates very serious errors.
*/
Error,
}
async function log(
level: LogLevel,
message: string,
options?: LogOptions
): Promise<void> {
const traces = new Error().stack?.split("\n").map((line) => line.split("@"));
const filtered = traces?.filter(([name, location]) => {
return name.length > 0 && location !== "[native code]";
});
const { file, line, ...keyValues } = options ?? {};
await invoke("plugin:log|log", {
level,
message,
location: filtered?.[0]?.filter((v) => v.length > 0).join("@"),
file,
line,
keyValues,
});
}
/**
* Logs a message at the error level.
*
* @param message
*
* # Examples
*
* ```js
* import { error } from 'tauri-plugin-log-api';
*
* const err_info = "No connection";
* const port = 22;
*
* error(`Error: ${err_info} on port ${port}`);
* ```
*/
export async function error(
message: string,
options?: LogOptions
): Promise<void> {
await log(LogLevel.Error, message, options);
}
/**
* Logs a message at the warn level.
*
* @param message
*
* # Examples
*
* ```js
* import { warn } from 'tauri-plugin-log-api';
*
* const warn_description = "Invalid Input";
*
* warn(`Warning! {warn_description}!`);
* ```
*/
export async function warn(
message: string,
options?: LogOptions
): Promise<void> {
await log(LogLevel.Warn, message, options);
}
/**
* Logs a message at the info level.
*
* @param message
*
* # Examples
*
* ```js
* import { info } from 'tauri-plugin-log-api';
*
* const conn_info = { port: 40, speed: 3.20 };
*
* info(`Connected to port {conn_info.port} at {conn_info.speed} Mb/s`);
* ```
*/
export async function info(
message: string,
options?: LogOptions
): Promise<void> {
await log(LogLevel.Info, message, options);
}
/**
* Logs a message at the debug level.
*
* @param message
*
* # Examples
*
* ```js
* import { debug } from 'tauri-plugin-log-api';
*
* const pos = { x: 3.234, y: -1.223 };
*
* debug(`New position: x: {pos.x}, y: {pos.y}`);
* ```
*/
export async function debug(
message: string,
options?: LogOptions
): Promise<void> {
await log(LogLevel.Debug, message, options);
}
/**
* Logs a message at the trace level.
*
* @param message
*
* # Examples
*
* ```js
* import { trace } from 'tauri-plugin-log-api';
*
* let pos = { x: 3.234, y: -1.223 };
*
* trace(`Position is: x: {pos.x}, y: {pos.y}`);
* ```
*/
export async function trace(
message: string,
options?: LogOptions
): Promise<void> {
await log(LogLevel.Trace, message, options);
}
interface RecordPayload {
level: LogLevel;
message: string;
}
export async function attachConsole(): Promise<UnlistenFn> {
return await listen("log://log", (event) => {
const payload = event.payload as RecordPayload;
switch (payload.level) {
case LogLevel.Trace:
console.log(payload.message);
break;
case LogLevel.Debug:
console.debug(payload.message);
break;
case LogLevel.Info:
console.info(payload.message);
break;
case LogLevel.Warn:
console.warn(payload.message);
break;
case LogLevel.Error:
console.error(payload.message);
break;
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`unknown log level ${payload.level}`);
}
});
}
+29
View File
@@ -0,0 +1,29 @@
{
"name": "tauri-plugin-log",
"version": "0.0.0",
"license": "MIT or APACHE-2.0",
"type": "module",
"browser": "dist/index.min.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"browser": "./dist/index.min.js"
},
"scripts": {
"build": "rollup -c"
},
"files": [
"dist",
"!dist/**/*.map",
"README.md",
"LICENSE"
],
"devDependencies": {
"tslib": "^2.4.1"
},
"dependencies": {
"@tauri-apps/api": "^1.2.0"
}
}
+10
View File
@@ -0,0 +1,10 @@
import { readFileSync } from "fs";
import { createConfig } from "../../../shared/rollup.config.mjs";
export default createConfig({
pkg: JSON.parse(
readFileSync(new URL("./package.json", import.meta.url), "utf8")
),
external: [/^@tauri-apps\/api/],
});
+1
View File
@@ -0,0 +1 @@
../../../shared/tsconfig.json
+350
View File
@@ -0,0 +1,350 @@
// Copyright 2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use fern::FormatCallback;
use log::{logger, RecordBuilder};
use log::{LevelFilter, Record};
use serde::Serialize;
use serde_repr::{Deserialize_repr, Serialize_repr};
use std::borrow::Cow;
use std::collections::HashMap;
use std::{
fmt::Arguments,
fs::{self, File},
iter::FromIterator,
path::{Path, PathBuf},
};
use tauri::{
plugin::{self, TauriPlugin},
Manager, Runtime,
};
pub use fern;
const DEFAULT_MAX_FILE_SIZE: u128 = 40000;
const DEFAULT_ROTATION_STRATEGY: RotationStrategy = RotationStrategy::KeepOne;
const DEFAULT_LOG_TARGETS: [LogTarget; 2] = [LogTarget::Stdout, LogTarget::LogDir];
/// An enum representing the available verbosity levels of the logger.
///
/// It is very similar to the [`log::Level`], but serializes to unsigned ints instead of strings.
#[derive(Debug, Clone, Deserialize_repr, Serialize_repr)]
#[repr(u16)]
pub enum LogLevel {
/// The "trace" level.
///
/// Designates very low priority, often extremely verbose, information.
Trace = 1,
/// The "debug" level.
///
/// Designates lower priority information.
Debug,
/// The "info" level.
///
/// Designates useful information.
Info,
/// The "warn" level.
///
/// Designates hazardous situations.
Warn,
/// The "error" level.
///
/// Designates very serious errors.
Error,
}
impl From<LogLevel> for log::Level {
fn from(log_level: LogLevel) -> Self {
match log_level {
LogLevel::Trace => log::Level::Trace,
LogLevel::Debug => log::Level::Debug,
LogLevel::Info => log::Level::Info,
LogLevel::Warn => log::Level::Warn,
LogLevel::Error => log::Level::Error,
}
}
}
impl From<log::Level> for LogLevel {
fn from(log_level: log::Level) -> Self {
match log_level {
log::Level::Trace => LogLevel::Trace,
log::Level::Debug => LogLevel::Debug,
log::Level::Info => LogLevel::Info,
log::Level::Warn => LogLevel::Warn,
log::Level::Error => LogLevel::Error,
}
}
}
pub enum RotationStrategy {
KeepAll,
KeepOne,
}
#[derive(Debug, Serialize, Clone)]
struct RecordPayload {
message: String,
level: LogLevel,
}
/// An enum representing the available targets of the logger.
pub enum LogTarget {
/// Print logs to stdout.
Stdout,
/// Print logs to stderr.
Stderr,
/// Write logs to the given directory.
///
/// The plugin will ensure the directory exists before writing logs.
Folder(PathBuf),
/// Write logs to the OS specififc logs directory.
///
/// ### Platform-specific
///
/// |Platform | Value | Example |
/// | ------- | --------------------------------------------- | ---------------------------------------------- |
/// | Linux | `{configDir}/{bundleIdentifier}` | `/home/alice/.config/com.tauri.dev` |
/// | macOS | `{homeDir}/Library/Logs/{bundleIdentifier}` | `/Users/Alice/Library/Logs/com.tauri.dev` |
/// | Windows | `{configDir}/{bundleIdentifier}` | `C:\Users\Alice\AppData\Roaming\com.tauri.dev` |
LogDir,
/// Forward logs to the webview (via the `log://log` event).
///
/// This requires the webview to subscribe to log events, via this plugins `attachConsole` function.
Webview,
}
#[tauri::command]
fn log(
level: LogLevel,
message: String,
location: Option<&str>,
file: Option<&str>,
line: Option<u32>,
key_values: Option<HashMap<String, String>>,
) {
let location = location.unwrap_or("webview");
let mut builder = RecordBuilder::new();
builder
.target(location)
.level(level.into())
.file(file)
.line(line);
let key_values = key_values.unwrap_or_default();
let mut kv = HashMap::new();
for (k, v) in key_values.iter() {
kv.insert(k.as_str(), v.as_str());
}
builder.key_values(&kv);
logger().log(&builder.args(format_args!("{message}")).build());
}
pub struct LoggerBuilder {
dispatch: fern::Dispatch,
rotation_strategy: RotationStrategy,
max_file_size: u128,
targets: Vec<LogTarget>,
}
impl Default for LoggerBuilder {
fn default() -> Self {
let format =
time::format_description::parse("[[[year]-[month]-[day]][[[hour]:[minute]:[second]]")
.unwrap();
let dispatch = fern::Dispatch::new().format(move |out, message, record| {
out.finish(format_args!(
"{}[{}][{}] {}",
time::OffsetDateTime::now_utc().format(&format).unwrap(),
record.target(),
record.level(),
message
))
});
Self {
dispatch,
rotation_strategy: DEFAULT_ROTATION_STRATEGY,
max_file_size: DEFAULT_MAX_FILE_SIZE,
targets: DEFAULT_LOG_TARGETS.into(),
}
}
}
impl LoggerBuilder {
pub fn new() -> Self {
Default::default()
}
pub fn rotation_strategy(mut self, rotation_strategy: RotationStrategy) -> Self {
self.rotation_strategy = rotation_strategy;
self
}
pub fn max_file_size(mut self, max_file_size: u128) -> Self {
self.max_file_size = max_file_size;
self
}
pub fn format<F>(mut self, formatter: F) -> Self
where
F: Fn(FormatCallback, &Arguments, &Record) + Sync + Send + 'static,
{
self.dispatch = self.dispatch.format(formatter);
self
}
pub fn level(mut self, level_filter: impl Into<LevelFilter>) -> Self {
self.dispatch = self.dispatch.level(level_filter.into());
self
}
pub fn level_for(mut self, module: impl Into<Cow<'static, str>>, level: LevelFilter) -> Self {
self.dispatch = self.dispatch.level_for(module, level);
self
}
pub fn filter<F>(mut self, filter: F) -> Self
where
F: Fn(&log::Metadata) -> bool + Send + Sync + 'static,
{
self.dispatch = self.dispatch.filter(filter);
self
}
pub fn target(mut self, target: LogTarget) -> Self {
self.targets.push(target);
self
}
pub fn targets(mut self, targets: impl IntoIterator<Item = LogTarget>) -> Self {
self.targets = Vec::from_iter(targets);
self
}
#[cfg(feature = "colored")]
pub fn with_colors(self, colors: fern::colors::ColoredLevelConfig) -> Self {
let format =
time::format_description::parse("[[[year]-[month]-[day]][[[hour]:[minute]:[second]]")
.unwrap();
self.format(move |out, message, record| {
out.finish(format_args!(
"{}[{}][{}] {}",
time::OffsetDateTime::now_utc().format(&format).unwrap(),
record.target(),
colors.color(record.level()),
message
))
})
}
pub fn build<R: Runtime>(mut self) -> TauriPlugin<R> {
plugin::Builder::new("log")
.invoke_handler(tauri::generate_handler![log])
.setup(move |app_handle| {
let app_name = &app_handle.package_info().name;
// setup targets
for target in &self.targets {
self.dispatch = self.dispatch.chain(match target {
LogTarget::Stdout => fern::Output::from(std::io::stdout()),
LogTarget::Stderr => fern::Output::from(std::io::stderr()),
LogTarget::Folder(path) => {
if !path.exists() {
fs::create_dir_all(&path).unwrap();
}
fern::log_file(get_log_file_path(
&path,
app_name,
&self.rotation_strategy,
self.max_file_size,
)?)?
.into()
}
LogTarget::LogDir => {
let path = app_handle.path_resolver().log_dir().unwrap();
if !path.exists() {
fs::create_dir_all(&path).unwrap();
}
fern::log_file(get_log_file_path(
&path,
app_name,
&self.rotation_strategy,
self.max_file_size,
)?)?
.into()
}
LogTarget::Webview => {
let app_handle = app_handle.clone();
fern::Output::call(move |record| {
let payload = RecordPayload {
message: record.args().to_string(),
level: record.level().into(),
};
let app_handle = app_handle.clone();
tauri::async_runtime::spawn(async move {
app_handle.emit_all("log://log", payload).unwrap();
});
})
}
});
}
self.dispatch.apply()?;
Ok(())
})
.build()
}
}
fn get_log_file_path(
dir: &impl AsRef<Path>,
app_name: &str,
rotation_strategy: &RotationStrategy,
max_file_size: u128,
) -> plugin::Result<PathBuf> {
let path = dir.as_ref().join(format!("{}.log", app_name));
if path.exists() {
let log_size = File::open(&path)?.metadata()?.len() as u128;
if log_size > max_file_size {
match rotation_strategy {
RotationStrategy::KeepAll => {
let to = dir.as_ref().join(format!(
"{}_{}.log",
app_name,
time::OffsetDateTime::now_utc()
.format(
&time::format_description::parse(
"[year]-[month]-[day]_[hour]-[minute]-[second]"
)
.unwrap()
)
.unwrap(),
));
if to.is_file() {
// designated rotated log file name already exists
// highly unlikely but defensively handle anyway by adding .bak to filename
let mut to_bak = to.clone();
to_bak.set_file_name(format!(
"{}.bak",
to_bak.file_name().unwrap().to_string_lossy()
));
fs::rename(&to, to_bak)?;
}
fs::rename(&path, to)?;
}
RotationStrategy::KeepOne => {
fs::remove_file(&path)?;
}
}
}
}
Ok(path)
}
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "tauri-plugin-persisted-scope"
version = "0.1.0"
edition.workspace = true
authors.workspace = true
license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde.workspace = true
serde_json.workspace = true
tauri.workspace = true
log.workspace = true
thiserror.workspace = true
bincode = "1"
[features]
protocol-asset = [ "tauri/protocol-asset" ]
+101
View File
@@ -0,0 +1,101 @@
// Copyright 2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::{Deserialize, Serialize};
use tauri::{
plugin::{Builder, TauriPlugin},
FsScopeEvent, Manager, Runtime,
};
use std::{
fs::{create_dir_all, File},
io::Write,
};
const SCOPE_STATE_FILENAME: &str = ".persisted-scope";
#[derive(Debug, thiserror::Error)]
enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Tauri(#[from] tauri::Error),
#[error(transparent)]
TauriApi(#[from] tauri::api::Error),
#[error(transparent)]
Bincode(#[from] Box<bincode::ErrorKind>),
}
#[derive(Debug, Default, Deserialize, Serialize)]
struct Scope {
allowed_paths: Vec<String>,
forbidden_patterns: Vec<String>,
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("persisted-scope")
.setup(|app| {
let fs_scope = app.fs_scope();
#[cfg(feature = "protocol-asset")]
let asset_protocol_scope = app.asset_protocol_scope();
let app = app.clone();
let app_dir = app.path_resolver().app_dir();
if let Some(app_dir) = app_dir {
let scope_state_path = app_dir.join(SCOPE_STATE_FILENAME);
let _ = fs_scope.forbid_file(&scope_state_path);
#[cfg(feature = "protocol-asset")]
let _ = asset_protocol_scope.forbid_file(&scope_state_path);
if scope_state_path.exists() {
let scope: Scope = tauri::api::file::read_binary(&scope_state_path)
.map_err(Error::from)
.and_then(|scope| bincode::deserialize(&scope).map_err(Into::into))
.unwrap_or_default();
for allowed in scope.allowed_paths {
// allows the path as is
let _ = fs_scope.allow_file(&allowed);
#[cfg(feature = "protocol-asset")]
let _ = asset_protocol_scope.allow_file(allowed);
}
for forbidden in scope.forbidden_patterns {
// forbid the path as is
let _ = fs_scope.forbid_file(&forbidden);
#[cfg(feature = "protocol-asset")]
let _ = asset_protocol_scope.forbid_file(forbidden);
}
}
fs_scope.listen(move |event| {
let fs_scope = app.fs_scope();
if let FsScopeEvent::PathAllowed(_) = event {
let scope = Scope {
allowed_paths: fs_scope
.allowed_patterns()
.into_iter()
.map(|p| p.to_string())
.collect(),
forbidden_patterns: fs_scope
.forbidden_patterns()
.into_iter()
.map(|p| p.to_string())
.collect(),
};
let scope_state_path = scope_state_path.clone();
let _ = create_dir_all(&app_dir)
.and_then(|_| File::create(scope_state_path))
.map_err(Error::Io)
.and_then(|mut f| {
f.write_all(&bincode::serialize(&scope).map_err(Error::from)?)
.map_err(Into::into)
});
}
});
}
Ok(())
})
.build()
}
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "tauri-plugin-positioner"
version = "0.1.0"
edition.workspace = true
authors.workspace = true
license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde.workspace = true
serde_json.workspace = true
tauri.workspace = true
log.workspace = true
thiserror.workspace = true
[features]
system-tray = [ "tauri/system-tray" ]
+40
View File
@@ -0,0 +1,40 @@
var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumerable:!0});};
var f={};e(f,{convertFileSrc:()=>w,invoke:()=>c,transformCallback:()=>s});function u(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function s(e,r=!1){let n=u(),t=`_${n}`;return Object.defineProperty(window,t,{value:o=>(r&&Reflect.deleteProperty(window,t),e==null?void 0:e(o)),writable:!1,configurable:!0}),n}async function c(e,r={}){return new Promise((n,t)=>{let o=s(i=>{n(i),Reflect.deleteProperty(window,`_${a}`);},!0),a=s(i=>{t(i),Reflect.deleteProperty(window,`_${o}`);},!0);window.__TAURI_IPC__({cmd:e,callback:o,error:a,...r});})}function w(e,r="asset"){let n=encodeURIComponent(e);return navigator.userAgent.includes("Windows")?`https://${r}.localhost/${n}`:`${r}://localhost/${n}`}
// Copyright 2021 Jonas Kruckenberg
/**
* Well known window positions.
*/
var Position;
(function (Position) {
Position[Position["TopLeft"] = 0] = "TopLeft";
Position[Position["TopRight"] = 1] = "TopRight";
Position[Position["BottomLeft"] = 2] = "BottomLeft";
Position[Position["BottomRight"] = 3] = "BottomRight";
Position[Position["TopCenter"] = 4] = "TopCenter";
Position[Position["BottomCenter"] = 5] = "BottomCenter";
Position[Position["LeftCenter"] = 6] = "LeftCenter";
Position[Position["RightCenter"] = 7] = "RightCenter";
Position[Position["Center"] = 8] = "Center";
Position[Position["TrayLeft"] = 9] = "TrayLeft";
Position[Position["TrayBottomLeft"] = 10] = "TrayBottomLeft";
Position[Position["TrayRight"] = 11] = "TrayRight";
Position[Position["TrayBottomRight"] = 12] = "TrayBottomRight";
Position[Position["TrayCenter"] = 13] = "TrayCenter";
Position[Position["TrayBottomCenter"] = 14] = "TrayBottomCenter";
})(Position || (Position = {}));
/**
* Moves the `Window` to the given {@link Position} using `WindowExt.move_window()`
* All positions are relative to the **current** screen.
*
* @param to The {@link Position} to move to.
*/
async function moveWindow(to) {
await c("plugin:positioner|move_window", {
position: to,
});
}
export { Position, moveWindow };
//# sourceMappingURL=index.min.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.min.js","sources":["../../../../node_modules/.pnpm/@tauri-apps+api@1.2.0/node_modules/@tauri-apps/api/chunk-FEIY7W7S.js","../../../../node_modules/.pnpm/@tauri-apps+api@1.2.0/node_modules/@tauri-apps/api/chunk-RCPA6UVN.js","../index.ts"],"sourcesContent":["var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumerable:!0})};export{e as a};\n","import{a as d}from\"./chunk-FEIY7W7S.js\";var f={};d(f,{convertFileSrc:()=>w,invoke:()=>c,transformCallback:()=>s});function u(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function s(e,r=!1){let n=u(),t=`_${n}`;return Object.defineProperty(window,t,{value:o=>(r&&Reflect.deleteProperty(window,t),e==null?void 0:e(o)),writable:!1,configurable:!0}),n}async function c(e,r={}){return new Promise((n,t)=>{let o=s(i=>{n(i),Reflect.deleteProperty(window,`_${a}`)},!0),a=s(i=>{t(i),Reflect.deleteProperty(window,`_${o}`)},!0);window.__TAURI_IPC__({cmd:e,callback:o,error:a,...r})})}function w(e,r=\"asset\"){let n=encodeURIComponent(e);return navigator.userAgent.includes(\"Windows\")?`https://${r}.localhost/${n}`:`${r}://localhost/${n}`}export{s as a,c as b,w as c,f as d};\n",null],"names":["d","invoke"],"mappings":"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"}
+38
View File
@@ -0,0 +1,38 @@
import { invoke } from '@tauri-apps/api/tauri';
// Copyright 2021 Jonas Kruckenberg
/**
* Well known window positions.
*/
var Position;
(function (Position) {
Position[Position["TopLeft"] = 0] = "TopLeft";
Position[Position["TopRight"] = 1] = "TopRight";
Position[Position["BottomLeft"] = 2] = "BottomLeft";
Position[Position["BottomRight"] = 3] = "BottomRight";
Position[Position["TopCenter"] = 4] = "TopCenter";
Position[Position["BottomCenter"] = 5] = "BottomCenter";
Position[Position["LeftCenter"] = 6] = "LeftCenter";
Position[Position["RightCenter"] = 7] = "RightCenter";
Position[Position["Center"] = 8] = "Center";
Position[Position["TrayLeft"] = 9] = "TrayLeft";
Position[Position["TrayBottomLeft"] = 10] = "TrayBottomLeft";
Position[Position["TrayRight"] = 11] = "TrayRight";
Position[Position["TrayBottomRight"] = 12] = "TrayBottomRight";
Position[Position["TrayCenter"] = 13] = "TrayCenter";
Position[Position["TrayBottomCenter"] = 14] = "TrayBottomCenter";
})(Position || (Position = {}));
/**
* Moves the `Window` to the given {@link Position} using `WindowExt.move_window()`
* All positions are relative to the **current** screen.
*
* @param to The {@link Position} to move to.
*/
async function moveWindow(to) {
await invoke("plugin:positioner|move_window", {
position: to,
});
}
export { Position, moveWindow };
//# sourceMappingURL=index.mjs.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.mjs","sources":["../index.ts"],"sourcesContent":[null],"names":[],"mappings":";;AAAA;AAKA;;AAEG;IACS,SAgBX;AAhBD,CAAA,UAAY,QAAQ,EAAA;AAClB,IAAA,QAAA,CAAA,QAAA,CAAA,SAAA,CAAA,GAAA,CAAA,CAAA,GAAA,SAAW,CAAA;AACX,IAAA,QAAA,CAAA,QAAA,CAAA,UAAA,CAAA,GAAA,CAAA,CAAA,GAAA,UAAQ,CAAA;AACR,IAAA,QAAA,CAAA,QAAA,CAAA,YAAA,CAAA,GAAA,CAAA,CAAA,GAAA,YAAU,CAAA;AACV,IAAA,QAAA,CAAA,QAAA,CAAA,aAAA,CAAA,GAAA,CAAA,CAAA,GAAA,aAAW,CAAA;AACX,IAAA,QAAA,CAAA,QAAA,CAAA,WAAA,CAAA,GAAA,CAAA,CAAA,GAAA,WAAS,CAAA;AACT,IAAA,QAAA,CAAA,QAAA,CAAA,cAAA,CAAA,GAAA,CAAA,CAAA,GAAA,cAAY,CAAA;AACZ,IAAA,QAAA,CAAA,QAAA,CAAA,YAAA,CAAA,GAAA,CAAA,CAAA,GAAA,YAAU,CAAA;AACV,IAAA,QAAA,CAAA,QAAA,CAAA,aAAA,CAAA,GAAA,CAAA,CAAA,GAAA,aAAW,CAAA;AACX,IAAA,QAAA,CAAA,QAAA,CAAA,QAAA,CAAA,GAAA,CAAA,CAAA,GAAA,QAAM,CAAA;AACN,IAAA,QAAA,CAAA,QAAA,CAAA,UAAA,CAAA,GAAA,CAAA,CAAA,GAAA,UAAQ,CAAA;AACR,IAAA,QAAA,CAAA,QAAA,CAAA,gBAAA,CAAA,GAAA,EAAA,CAAA,GAAA,gBAAc,CAAA;AACd,IAAA,QAAA,CAAA,QAAA,CAAA,WAAA,CAAA,GAAA,EAAA,CAAA,GAAA,WAAS,CAAA;AACT,IAAA,QAAA,CAAA,QAAA,CAAA,iBAAA,CAAA,GAAA,EAAA,CAAA,GAAA,iBAAe,CAAA;AACf,IAAA,QAAA,CAAA,QAAA,CAAA,YAAA,CAAA,GAAA,EAAA,CAAA,GAAA,YAAU,CAAA;AACV,IAAA,QAAA,CAAA,QAAA,CAAA,kBAAA,CAAA,GAAA,EAAA,CAAA,GAAA,kBAAgB,CAAA;AAClB,CAAC,EAhBW,QAAQ,KAAR,QAAQ,GAgBnB,EAAA,CAAA,CAAA,CAAA;AAED;;;;;AAKG;AACI,eAAe,UAAU,CAAC,EAAY,EAAA;IAC3C,MAAM,MAAM,CAAC,+BAA+B,EAAE;AAC5C,QAAA,QAAQ,EAAE,EAAE;AACb,KAAA,CAAC,CAAC;AACL;;;;"}
+37
View File
@@ -0,0 +1,37 @@
// Copyright 2021 Jonas Kruckenberg
// SPDX-License-Identifier: MIT
import { invoke } from "@tauri-apps/api/tauri";
/**
* Well known window positions.
*/
export enum Position {
TopLeft = 0,
TopRight,
BottomLeft,
BottomRight,
TopCenter,
BottomCenter,
LeftCenter,
RightCenter,
Center,
TrayLeft,
TrayBottomLeft,
TrayRight,
TrayBottomRight,
TrayCenter,
TrayBottomCenter,
}
/**
* Moves the `Window` to the given {@link Position} using `WindowExt.move_window()`
* All positions are relative to the **current** screen.
*
* @param to The {@link Position} to move to.
*/
export async function moveWindow(to: Position): Promise<void> {
await invoke("plugin:positioner|move_window", {
position: to,
});
}
+29
View File
@@ -0,0 +1,29 @@
{
"name": "tauri-plugin-positioner-api",
"version": "0.0.0",
"license": "MIT or APACHE-2.0",
"type": "module",
"browser": "dist/index.min.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"browser": "./dist/index.min.js"
},
"scripts": {
"build": "rollup -c"
},
"files": [
"dist",
"!dist/**/*.map",
"README.md",
"LICENSE"
],
"devDependencies": {
"tslib": "^2.4.1"
},
"dependencies": {
"@tauri-apps/api": "^1.2.0"
}
}
@@ -0,0 +1,10 @@
import { readFileSync } from "fs";
import { createConfig } from "../../../shared/rollup.config.mjs";
export default createConfig({
pkg: JSON.parse(
readFileSync(new URL("./package.json", import.meta.url), "utf8")
),
external: [/^@tauri-apps\/api/],
});
+1
View File
@@ -0,0 +1 @@
../../../shared/tsconfig.json
+182
View File
@@ -0,0 +1,182 @@
// Copyright 2021 Jonas Kruckenberg
// SPDX-License-Identifier: MIT
#[cfg(feature = "system-tray")]
use crate::Tray;
use serde_repr::Deserialize_repr;
#[cfg(feature = "system-tray")]
use tauri::Manager;
use tauri::{PhysicalPosition, PhysicalSize, Result, Runtime, Window};
/// Well known window positions.
#[derive(Debug, Deserialize_repr)]
#[repr(u16)]
pub enum Position {
TopLeft = 0,
TopRight,
BottomLeft,
BottomRight,
TopCenter,
BottomCenter,
LeftCenter,
RightCenter,
Center,
#[cfg(feature = "system-tray")]
TrayLeft,
#[cfg(feature = "system-tray")]
TrayBottomLeft,
#[cfg(feature = "system-tray")]
TrayRight,
#[cfg(feature = "system-tray")]
TrayBottomRight,
#[cfg(feature = "system-tray")]
TrayCenter,
#[cfg(feature = "system-tray")]
TrayBottomCenter,
}
/// A [`Window`] extension that provides extra methods related to positioning.
pub trait WindowExt {
/// Moves the [`Window`] to the given [`Position`]
///
/// All positions are relative to the **current** screen.
fn move_window(&self, position: Position) -> Result<()>;
}
impl<R: Runtime> WindowExt for Window<R> {
fn move_window(&self, pos: Position) -> Result<()> {
use Position::*;
let screen = self.current_monitor()?.unwrap();
let screen_position = screen.position();
let screen_size = PhysicalSize::<i32> {
width: screen.size().width as i32,
height: screen.size().height as i32,
};
let window_size = PhysicalSize::<i32> {
width: self.outer_size()?.width as i32,
height: self.outer_size()?.height as i32,
};
#[cfg(feature = "system-tray")]
let (tray_position, tray_size) = self
.state::<Tray>()
.0
.lock()
.unwrap()
.map(|(pos, size)| {
(
Some((pos.x as i32, pos.y as i32)),
Some((size.width as i32, size.height as i32)),
)
})
.unwrap_or_default();
let physical_pos = match pos {
TopLeft => *screen_position,
TopRight => PhysicalPosition {
x: screen_position.x + (screen_size.width - window_size.width),
y: screen_position.y,
},
BottomLeft => PhysicalPosition {
x: screen_position.x,
y: screen_size.height - (window_size.height - screen_position.y),
},
BottomRight => PhysicalPosition {
x: screen_position.x + (screen_size.width - window_size.width),
y: screen_size.height - (window_size.height - screen_position.y),
},
TopCenter => PhysicalPosition {
x: screen_position.x + ((screen_size.width / 2) - (window_size.width / 2)),
y: screen_position.y,
},
BottomCenter => PhysicalPosition {
x: screen_position.x + ((screen_size.width / 2) - (window_size.width / 2)),
y: screen_size.height - (window_size.height - screen_position.y),
},
LeftCenter => PhysicalPosition {
x: screen_position.x,
y: screen_position.y + (screen_size.height / 2) - (window_size.height / 2),
},
RightCenter => PhysicalPosition {
x: screen_position.x + (screen_size.width - window_size.width),
y: screen_position.y + (screen_size.height / 2) - (window_size.height / 2),
},
Center => PhysicalPosition {
x: screen_position.x + ((screen_size.width / 2) - (window_size.width / 2)),
y: screen_position.y + (screen_size.height / 2) - (window_size.height / 2),
},
#[cfg(feature = "system-tray")]
TrayLeft => {
if let Some((tray_x, tray_y)) = tray_position {
PhysicalPosition {
x: tray_x,
y: tray_y - window_size.height,
}
} else {
panic!("tray position not set");
}
}
#[cfg(feature = "system-tray")]
TrayBottomLeft => {
if let Some((tray_x, tray_y)) = tray_position {
PhysicalPosition {
x: tray_x,
y: tray_y,
}
} else {
panic!("Tray position not set");
}
}
#[cfg(feature = "system-tray")]
TrayRight => {
if let (Some((tray_x, tray_y)), Some((tray_width, _))) = (tray_position, tray_size)
{
PhysicalPosition {
x: tray_x + tray_width,
y: tray_y - window_size.height,
}
} else {
panic!("Tray position not set");
}
}
#[cfg(feature = "system-tray")]
TrayBottomRight => {
if let (Some((tray_x, tray_y)), Some((tray_width, _))) = (tray_position, tray_size)
{
PhysicalPosition {
x: tray_x + tray_width,
y: tray_y,
}
} else {
panic!("Tray position not set");
}
}
#[cfg(feature = "system-tray")]
TrayCenter => {
if let (Some((tray_x, tray_y)), Some((tray_width, _))) = (tray_position, tray_size)
{
PhysicalPosition {
x: tray_x + (tray_width / 2) - (window_size.width / 2),
y: tray_y - window_size.height,
}
} else {
panic!("Tray position not set");
}
}
#[cfg(feature = "system-tray")]
TrayBottomCenter => {
if let (Some((tray_x, tray_y)), Some((tray_width, _))) = (tray_position, tray_size)
{
PhysicalPosition {
x: tray_x + (tray_width / 2) - (window_size.width / 2),
y: tray_y,
}
} else {
panic!("Tray position not set");
}
}
};
self.set_position(tauri::Position::Physical(physical_pos))
}
}
+71
View File
@@ -0,0 +1,71 @@
// Copyright 2021 Jonas Kruckenberg
// SPDX-License-Identifier: MIT
//! A plugin for Tauri that helps position your windows at well-known locations.
//!
//! # Cargo features
//!
//! - **system-tray**: Enables system-tray-relative positions.
//!
//! Note: This requires attaching the Tauri plugin, *even* when using the trait extension only.
mod ext;
pub use ext::*;
use tauri::{
plugin::{self, TauriPlugin},
Result, Runtime,
};
#[cfg(feature = "system-tray")]
use tauri::{AppHandle, Manager, PhysicalPosition, PhysicalSize, SystemTrayEvent};
#[cfg(feature = "system-tray")]
struct Tray(std::sync::Mutex<Option<(PhysicalPosition<f64>, PhysicalSize<f64>)>>);
#[cfg(feature = "system-tray")]
pub fn on_tray_event<R: Runtime>(app: &AppHandle<R>, event: &SystemTrayEvent) {
match event {
SystemTrayEvent::LeftClick { position, size, .. } => {
app.state::<Tray>()
.0
.lock()
.unwrap()
.replace((*position, *size));
}
SystemTrayEvent::RightClick { position, size, .. } => {
app.state::<Tray>()
.0
.lock()
.unwrap()
.replace((*position, *size));
}
SystemTrayEvent::DoubleClick { position, size, .. } => {
app.state::<Tray>()
.0
.lock()
.unwrap()
.replace((*position, *size));
}
_ => (),
}
}
#[tauri::command]
async fn move_window<R: Runtime>(window: tauri::Window<R>, position: Position) -> Result<()> {
window.move_window(position)
}
/// The Tauri plugin that exposes [`WindowExt::move_window`] to the webview.
pub fn init<R: Runtime>() -> TauriPlugin<R> {
let plugin =
plugin::Builder::new("positioner").invoke_handler(tauri::generate_handler![move_window]);
#[cfg(feature = "system-tray")]
let plugin = plugin.setup(|app_handle| {
app_handle.manage(Tray(std::sync::Mutex::new(None)));
Ok(())
});
plugin.build()
}
+23
View File
@@ -0,0 +1,23 @@
[package]
name = "tauri-plugin-sql"
version = "0.1.0"
edition.workspace = true
authors.workspace = true
license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde.workspace = true
serde_json.workspace = true
tauri.workspace = true
log.workspace = true
thiserror.workspace = true
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "json"] }
tokio = { version = "1", features = ["sync"] }
futures = "0.3"
[features]
sqlite = ["sqlx/sqlite"]
mysql = ["sqlx/mysql"]
postgres = ["sqlx/postgres"]
+119
View File
@@ -0,0 +1,119 @@
var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumerable:!0});};
var f={};e(f,{convertFileSrc:()=>w,invoke:()=>c,transformCallback:()=>s});function u(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function s(e,r=!1){let n=u(),t=`_${n}`;return Object.defineProperty(window,t,{value:o=>(r&&Reflect.deleteProperty(window,t),e==null?void 0:e(o)),writable:!1,configurable:!0}),n}async function c(e,r={}){return new Promise((n,t)=>{let o=s(i=>{n(i),Reflect.deleteProperty(window,`_${a}`);},!0),a=s(i=>{t(i),Reflect.deleteProperty(window,`_${o}`);},!0);window.__TAURI_IPC__({cmd:e,callback:o,error:a,...r});})}function w(e,r="asset"){let n=encodeURIComponent(e);return navigator.userAgent.includes("Windows")?`https://${r}.localhost/${n}`:`${r}://localhost/${n}`}
/**
* **Database**
*
* The `Database` class serves as the primary interface for
* communicating with the rust side of the sql plugin.
*/
class Database {
constructor(path) {
this.path = path;
}
/**
* **load**
*
* A static initializer which connects to the underlying database and
* returns a `Database` instance once a connection to the database is established.
*
* # Sqlite
*
* The path is relative to `tauri::api::path::BaseDirectory::App` and must start with `sqlite:`.
*
* @example
* ```ts
* const db = await Database.load("sqlite:test.db");
* ```
*/
static async load(path) {
const _path = await c("plugin:sql|load", {
db: path,
});
return new Database(_path);
}
/**
* **get**
*
* A static initializer which synchronously returns an instance of
* the Database class while deferring the actual database connection
* until the first invocation or selection on the database.
*
* # Sqlite
*
* The path is relative to `tauri::api::path::BaseDirectory::App` and must start with `sqlite:`.
*
* @example
* ```ts
* const db = Database.get("sqlite:test.db");
* ```
*/
static get(path) {
return new Database(path);
}
/**
* **execute**
*
* Passes a SQL expression to the database for execution.
*
* @example
* ```ts
* const result = await db.execute(
* "UPDATE todos SET title = $1, completed = $2 WHERE id = $3",
* [ todos.title, todos.status, todos.id ]
* );
* ```
*/
async execute(query, bindValues) {
const [rowsAffected, lastInsertId] = await c("plugin:sql|execute", {
db: this.path,
query,
values: bindValues !== null && bindValues !== void 0 ? bindValues : [],
});
return {
lastInsertId,
rowsAffected,
};
}
/**
* **select**
*
* Passes in a SELECT query to the database for execution.
*
* @example
* ```ts
* const result = await db.select(
* "SELECT * from todos WHERE id = $1", id
* );
* ```
*/
async select(query, bindValues) {
const result = await c("plugin:sql|select", {
db: this.path,
query,
values: bindValues !== null && bindValues !== void 0 ? bindValues : [],
});
return result;
}
/**
* **close**
*
* Closes the database connection pool.
*
* @example
* ```ts
* const success = await db.close()
* ```
* @param db - Optionally state the name of a database if you are managing more than one. Otherwise, all database pools will be in scope.
*/
async close(db) {
const success = await c("plugin:sql|close", {
db,
});
return success;
}
}
export { Database as default };
//# sourceMappingURL=index.min.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.min.js","sources":["../../../../node_modules/.pnpm/@tauri-apps+api@1.2.0/node_modules/@tauri-apps/api/chunk-FEIY7W7S.js","../../../../node_modules/.pnpm/@tauri-apps+api@1.2.0/node_modules/@tauri-apps/api/chunk-RCPA6UVN.js","../index.ts"],"sourcesContent":["var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumerable:!0})};export{e as a};\n","import{a as d}from\"./chunk-FEIY7W7S.js\";var f={};d(f,{convertFileSrc:()=>w,invoke:()=>c,transformCallback:()=>s});function u(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function s(e,r=!1){let n=u(),t=`_${n}`;return Object.defineProperty(window,t,{value:o=>(r&&Reflect.deleteProperty(window,t),e==null?void 0:e(o)),writable:!1,configurable:!0}),n}async function c(e,r={}){return new Promise((n,t)=>{let o=s(i=>{n(i),Reflect.deleteProperty(window,`_${a}`)},!0),a=s(i=>{t(i),Reflect.deleteProperty(window,`_${o}`)},!0);window.__TAURI_IPC__({cmd:e,callback:o,error:a,...r})})}function w(e,r=\"asset\"){let n=encodeURIComponent(e);return navigator.userAgent.includes(\"Windows\")?`https://${r}.localhost/${n}`:`${r}://localhost/${n}`}export{s as a,c as b,w as c,f as d};\n",null],"names":["d","invoke"],"mappings":"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"}
+117
View File
@@ -0,0 +1,117 @@
import { invoke } from '@tauri-apps/api/tauri';
/**
* **Database**
*
* The `Database` class serves as the primary interface for
* communicating with the rust side of the sql plugin.
*/
class Database {
constructor(path) {
this.path = path;
}
/**
* **load**
*
* A static initializer which connects to the underlying database and
* returns a `Database` instance once a connection to the database is established.
*
* # Sqlite
*
* The path is relative to `tauri::api::path::BaseDirectory::App` and must start with `sqlite:`.
*
* @example
* ```ts
* const db = await Database.load("sqlite:test.db");
* ```
*/
static async load(path) {
const _path = await invoke("plugin:sql|load", {
db: path,
});
return new Database(_path);
}
/**
* **get**
*
* A static initializer which synchronously returns an instance of
* the Database class while deferring the actual database connection
* until the first invocation or selection on the database.
*
* # Sqlite
*
* The path is relative to `tauri::api::path::BaseDirectory::App` and must start with `sqlite:`.
*
* @example
* ```ts
* const db = Database.get("sqlite:test.db");
* ```
*/
static get(path) {
return new Database(path);
}
/**
* **execute**
*
* Passes a SQL expression to the database for execution.
*
* @example
* ```ts
* const result = await db.execute(
* "UPDATE todos SET title = $1, completed = $2 WHERE id = $3",
* [ todos.title, todos.status, todos.id ]
* );
* ```
*/
async execute(query, bindValues) {
const [rowsAffected, lastInsertId] = await invoke("plugin:sql|execute", {
db: this.path,
query,
values: bindValues !== null && bindValues !== void 0 ? bindValues : [],
});
return {
lastInsertId,
rowsAffected,
};
}
/**
* **select**
*
* Passes in a SELECT query to the database for execution.
*
* @example
* ```ts
* const result = await db.select(
* "SELECT * from todos WHERE id = $1", id
* );
* ```
*/
async select(query, bindValues) {
const result = await invoke("plugin:sql|select", {
db: this.path,
query,
values: bindValues !== null && bindValues !== void 0 ? bindValues : [],
});
return result;
}
/**
* **close**
*
* Closes the database connection pool.
*
* @example
* ```ts
* const success = await db.close()
* ```
* @param db - Optionally state the name of a database if you are managing more than one. Otherwise, all database pools will be in scope.
*/
async close(db) {
const success = await invoke("plugin:sql|close", {
db,
});
return success;
}
}
export { Database as default };
//# sourceMappingURL=index.mjs.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.mjs","sources":["../index.ts"],"sourcesContent":[null],"names":[],"mappings":";;AAgBA;;;;;AAKG;AACW,MAAO,QAAQ,CAAA;AAE3B,IAAA,WAAA,CAAY,IAAY,EAAA;AACtB,QAAA,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;KAClB;AAED;;;;;;;;;;;;;;AAcG;AACH,IAAA,aAAa,IAAI,CAAC,IAAY,EAAA;AAC5B,QAAA,MAAM,KAAK,GAAG,MAAM,MAAM,CAAS,iBAAiB,EAAE;AACpD,YAAA,EAAE,EAAE,IAAI;AACT,SAAA,CAAC,CAAC;AAEH,QAAA,OAAO,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAC;KAC5B;AAED;;;;;;;;;;;;;;;AAeG;IACH,OAAO,GAAG,CAAC,IAAY,EAAA;AACrB,QAAA,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC;KAC3B;AAED;;;;;;;;;;;;AAYG;AACH,IAAA,MAAM,OAAO,CAAC,KAAa,EAAE,UAAsB,EAAA;QACjD,MAAM,CAAC,YAAY,EAAE,YAAY,CAAC,GAAG,MAAM,MAAM,CAC/C,oBAAoB,EACpB;YACE,EAAE,EAAE,IAAI,CAAC,IAAI;YACb,KAAK;AACL,YAAA,MAAM,EAAE,UAAU,KAAA,IAAA,IAAV,UAAU,KAAV,KAAA,CAAA,GAAA,UAAU,GAAI,EAAE;AACzB,SAAA,CACF,CAAC;QAEF,OAAO;YACL,YAAY;YACZ,YAAY;SACb,CAAC;KACH;AAED;;;;;;;;;;;AAWG;AACH,IAAA,MAAM,MAAM,CAAI,KAAa,EAAE,UAAsB,EAAA;AACnD,QAAA,MAAM,MAAM,GAAG,MAAM,MAAM,CAAI,mBAAmB,EAAE;YAClD,EAAE,EAAE,IAAI,CAAC,IAAI;YACb,KAAK;AACL,YAAA,MAAM,EAAE,UAAU,KAAA,IAAA,IAAV,UAAU,KAAV,KAAA,CAAA,GAAA,UAAU,GAAI,EAAE;AACzB,SAAA,CAAC,CAAC;AAEH,QAAA,OAAO,MAAM,CAAC;KACf;AAED;;;;;;;;;;AAUG;IACH,MAAM,KAAK,CAAC,EAAW,EAAA;AACrB,QAAA,MAAM,OAAO,GAAG,MAAM,MAAM,CAAU,kBAAkB,EAAE;YACxD,EAAE;AACH,SAAA,CAAC,CAAC;AACH,QAAA,OAAO,OAAO,CAAC;KAChB;AACF;;;;"}
+140
View File
@@ -0,0 +1,140 @@
import { invoke } from "@tauri-apps/api/tauri";
export interface QueryResult {
/** The number of rows affected by the query. */
rowsAffected: number;
/**
* The last inserted `id`.
*
* This value is always `0` when using the Postgres driver. If the
* last inserted id is required on Postgres, the `select` function
* must be used, with a `RETURNING` clause
* (`INSERT INTO todos (title) VALUES ($1) RETURNING id`).
*/
lastInsertId: number;
}
/**
* **Database**
*
* The `Database` class serves as the primary interface for
* communicating with the rust side of the sql plugin.
*/
export default class Database {
path: string;
constructor(path: string) {
this.path = path;
}
/**
* **load**
*
* A static initializer which connects to the underlying database and
* returns a `Database` instance once a connection to the database is established.
*
* # Sqlite
*
* The path is relative to `tauri::api::path::BaseDirectory::App` and must start with `sqlite:`.
*
* @example
* ```ts
* const db = await Database.load("sqlite:test.db");
* ```
*/
static async load(path: string): Promise<Database> {
const _path = await invoke<string>("plugin:sql|load", {
db: path,
});
return new Database(_path);
}
/**
* **get**
*
* A static initializer which synchronously returns an instance of
* the Database class while deferring the actual database connection
* until the first invocation or selection on the database.
*
* # Sqlite
*
* The path is relative to `tauri::api::path::BaseDirectory::App` and must start with `sqlite:`.
*
* @example
* ```ts
* const db = Database.get("sqlite:test.db");
* ```
*/
static get(path: string): Database {
return new Database(path);
}
/**
* **execute**
*
* Passes a SQL expression to the database for execution.
*
* @example
* ```ts
* const result = await db.execute(
* "UPDATE todos SET title = $1, completed = $2 WHERE id = $3",
* [ todos.title, todos.status, todos.id ]
* );
* ```
*/
async execute(query: string, bindValues?: unknown[]): Promise<QueryResult> {
const [rowsAffected, lastInsertId] = await invoke<[number, number]>(
"plugin:sql|execute",
{
db: this.path,
query,
values: bindValues ?? [],
}
);
return {
lastInsertId,
rowsAffected,
};
}
/**
* **select**
*
* Passes in a SELECT query to the database for execution.
*
* @example
* ```ts
* const result = await db.select(
* "SELECT * from todos WHERE id = $1", id
* );
* ```
*/
async select<T>(query: string, bindValues?: unknown[]): Promise<T> {
const result = await invoke<T>("plugin:sql|select", {
db: this.path,
query,
values: bindValues ?? [],
});
return result;
}
/**
* **close**
*
* Closes the database connection pool.
*
* @example
* ```ts
* const success = await db.close()
* ```
* @param db - Optionally state the name of a database if you are managing more than one. Otherwise, all database pools will be in scope.
*/
async close(db?: string): Promise<boolean> {
const success = await invoke<boolean>("plugin:sql|close", {
db,
});
return success;
}
}
+29
View File
@@ -0,0 +1,29 @@
{
"name": "tauri-plugin-{{name}}-api",
"version": "0.0.0",
"license": "MIT or APACHE-2.0",
"type": "module",
"browser": "dist/index.min.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"browser": "./dist/index.min.js"
},
"scripts": {
"build": "rollup -c"
},
"files": [
"dist",
"!dist/**/*.map",
"README.md",
"LICENSE"
],
"devDependencies": {
"tslib": "^2.4.1"
},
"dependencies": {
"@tauri-apps/api": "^1.2.0"
}
}
+10
View File
@@ -0,0 +1,10 @@
import { readFileSync } from "fs";
import { createConfig } from "../../../shared/rollup.config.mjs";
export default createConfig({
pkg: JSON.parse(
readFileSync(new URL("./package.json", import.meta.url), "utf8")
),
external: [/^@tauri-apps\/api/],
});
+1
View File
@@ -0,0 +1 @@
../../../shared/tsconfig.json
+28
View File
@@ -0,0 +1,28 @@
// Copyright 2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
#[cfg(any(
all(feature = "sqlite", feature = "mysql"),
all(feature = "sqlite", feature = "postgres"),
all(feature = "mysql", feature = "postgres")
))]
compile_error!("Only one database driver can be enabled. Use `default-features = false` and set the feature flag for the driver of your choice.");
#[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgres")))]
compile_error!(
"Database driver not defined. Please set the feature flag for the driver of your choice."
);
#[cfg(any(
all(feature = "sqlite", not(any(feature = "mysql", feature = "postgres"))),
all(feature = "mysql", not(any(feature = "sqlite", feature = "postgres"))),
all(feature = "postgres", not(any(feature = "sqlite", feature = "mysql"))),
))]
mod plugin;
#[cfg(any(
all(feature = "sqlite", not(any(feature = "mysql", feature = "postgres"))),
all(feature = "mysql", not(any(feature = "sqlite", feature = "postgres"))),
all(feature = "postgres", not(any(feature = "sqlite", feature = "mysql"))),
))]
pub use plugin::*;
+387
View File
@@ -0,0 +1,387 @@
// Copyright 2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use futures::future::BoxFuture;
use serde::{ser::Serializer, Deserialize, Serialize};
use serde_json::Value as JsonValue;
use sqlx::{
error::BoxDynError,
migrate::{
MigrateDatabase, Migration as SqlxMigration, MigrationSource, MigrationType, Migrator,
},
Column, Pool, Row, TypeInfo,
};
use tauri::{
command,
plugin::{Plugin, Result as PluginResult},
AppHandle, Invoke, Manager, RunEvent, Runtime, State,
};
use tokio::sync::Mutex;
use std::collections::HashMap;
#[cfg(feature = "sqlite")]
use std::{fs::create_dir_all, path::PathBuf};
#[cfg(feature = "sqlite")]
type Db = sqlx::sqlite::Sqlite;
#[cfg(feature = "mysql")]
type Db = sqlx::mysql::MySql;
#[cfg(feature = "postgres")]
type Db = sqlx::postgres::Postgres;
#[cfg(feature = "sqlite")]
type LastInsertId = i64;
#[cfg(not(feature = "sqlite"))]
type LastInsertId = u64;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Sql(#[from] sqlx::Error),
#[error(transparent)]
Migration(#[from] sqlx::migrate::MigrateError),
#[error("database {0} not loaded")]
DatabaseNotLoaded(String),
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
type Result<T> = std::result::Result<T, Error>;
#[cfg(feature = "sqlite")]
/// Resolves the App's **file path** from the `AppHandle` context
/// object
fn app_path<R: Runtime>(app: &AppHandle<R>) -> PathBuf {
#[allow(deprecated)] // FIXME: Change to non-deprecated function in Tauri v2
app.path_resolver()
.app_dir()
.expect("No App path was found!")
}
#[cfg(feature = "sqlite")]
/// Maps the user supplied DB connection string to a connection string
/// with a fully qualified file path to the App's designed "app_path"
fn path_mapper(mut app_path: PathBuf, connection_string: &str) -> String {
app_path.push(
connection_string
.split_once(':')
.expect("Couldn't parse the connection string for DB!")
.1,
);
format!(
"sqlite:{}",
app_path
.to_str()
.expect("Problem creating fully qualified path to Database file!")
)
}
#[derive(Default)]
struct DbInstances(Mutex<HashMap<String, Pool<Db>>>);
struct Migrations(Mutex<HashMap<String, MigrationList>>);
#[derive(Default, Deserialize)]
struct PluginConfig {
#[serde(default)]
preload: Vec<String>,
}
#[derive(Debug)]
pub enum MigrationKind {
Up,
Down,
}
impl From<MigrationKind> for MigrationType {
fn from(kind: MigrationKind) -> Self {
match kind {
MigrationKind::Up => Self::ReversibleUp,
MigrationKind::Down => Self::ReversibleDown,
}
}
}
/// A migration definition.
#[derive(Debug)]
pub struct Migration {
pub version: i64,
pub description: &'static str,
pub sql: &'static str,
pub kind: MigrationKind,
}
#[derive(Debug)]
struct MigrationList(Vec<Migration>);
impl MigrationSource<'static> for MigrationList {
fn resolve(self) -> BoxFuture<'static, std::result::Result<Vec<SqlxMigration>, BoxDynError>> {
Box::pin(async move {
let mut migrations = Vec::new();
for migration in self.0 {
if matches!(migration.kind, MigrationKind::Up) {
migrations.push(SqlxMigration::new(
migration.version,
migration.description.into(),
migration.kind.into(),
migration.sql.into(),
));
}
}
Ok(migrations)
})
}
}
#[command]
async fn load<R: Runtime>(
#[allow(unused_variables)] app: AppHandle<R>,
db_instances: State<'_, DbInstances>,
migrations: State<'_, Migrations>,
db: String,
) -> Result<String> {
#[cfg(feature = "sqlite")]
let fqdb = path_mapper(app_path(&app), &db);
#[cfg(not(feature = "sqlite"))]
let fqdb = db.clone();
#[cfg(feature = "sqlite")]
create_dir_all(app_path(&app)).expect("Problem creating App directory!");
if !Db::database_exists(&fqdb).await.unwrap_or(false) {
Db::create_database(&fqdb).await?;
}
let pool = Pool::connect(&fqdb).await?;
if let Some(migrations) = migrations.0.lock().await.remove(&db) {
let migrator = Migrator::new(migrations).await?;
migrator.run(&pool).await?;
}
db_instances.0.lock().await.insert(db.clone(), pool);
Ok(db)
}
/// Allows the database connection(s) to be closed; if no database
/// name is passed in then _all_ database connection pools will be
/// shut down.
#[command]
async fn close(db_instances: State<'_, DbInstances>, db: Option<String>) -> Result<bool> {
let mut instances = db_instances.0.lock().await;
let pools = if let Some(db) = db {
vec![db]
} else {
instances.keys().cloned().collect()
};
for pool in pools {
let db = instances
.get_mut(&pool) //
.ok_or(Error::DatabaseNotLoaded(pool))?;
db.close().await;
}
Ok(true)
}
/// Execute a command against the database
#[command]
async fn execute(
db_instances: State<'_, DbInstances>,
db: String,
query: String,
values: Vec<JsonValue>,
) -> Result<(u64, LastInsertId)> {
let mut instances = db_instances.0.lock().await;
let db = instances.get_mut(&db).ok_or(Error::DatabaseNotLoaded(db))?;
let mut query = sqlx::query(&query);
for value in values {
if value.is_string() {
query = query.bind(value.as_str().unwrap().to_owned())
} else {
query = query.bind(value);
}
}
let result = query.execute(&*db).await?;
#[cfg(feature = "sqlite")]
let r = Ok((result.rows_affected(), result.last_insert_rowid()));
#[cfg(feature = "mysql")]
let r = Ok((result.rows_affected(), result.last_insert_id()));
#[cfg(feature = "postgres")]
let r = Ok((result.rows_affected(), 0));
r
}
#[command]
async fn select(
db_instances: State<'_, DbInstances>,
db: String,
query: String,
values: Vec<JsonValue>,
) -> Result<Vec<HashMap<String, JsonValue>>> {
let mut instances = db_instances.0.lock().await;
let db = instances.get_mut(&db).ok_or(Error::DatabaseNotLoaded(db))?;
let mut query = sqlx::query(&query);
for value in values {
if value.is_string() {
query = query.bind(value.as_str().unwrap().to_owned())
} else {
query = query.bind(value);
}
}
let rows = query.fetch_all(&*db).await?;
let mut values = Vec::new();
for row in rows {
let mut value = HashMap::default();
for (i, column) in row.columns().iter().enumerate() {
let info = column.type_info();
let v = if info.is_null() {
JsonValue::Null
} else {
match info.name() {
"VARCHAR" | "STRING" | "TEXT" | "DATETIME" => {
if let Ok(s) = row.try_get(i) {
JsonValue::String(s)
} else {
JsonValue::Null
}
}
"BOOL" | "BOOLEAN" => {
if let Ok(b) = row.try_get(i) {
JsonValue::Bool(b)
} else {
let x: String = row.get(i);
JsonValue::Bool(x.to_lowercase() == "true")
}
}
"INT" | "NUMBER" | "INTEGER" | "BIGINT" | "INT8" => {
if let Ok(n) = row.try_get::<i64, usize>(i) {
JsonValue::Number(n.into())
} else {
JsonValue::Null
}
}
"REAL" => {
if let Ok(n) = row.try_get::<f64, usize>(i) {
JsonValue::from(n)
} else {
JsonValue::Null
}
}
// "JSON" => JsonValue::Object(row.get(i)),
"BLOB" => {
if let Ok(n) = row.try_get::<Vec<u8>, usize>(i) {
JsonValue::Array(
n.into_iter().map(|n| JsonValue::Number(n.into())).collect(),
)
} else {
JsonValue::Null
}
}
_ => JsonValue::Null,
}
};
value.insert(column.name().to_string(), v);
}
values.push(value);
}
Ok(values)
}
/// Tauri SQL plugin.
pub struct TauriSql<R: Runtime> {
migrations: Option<HashMap<String, MigrationList>>,
invoke_handler: Box<dyn Fn(Invoke<R>) + Send + Sync>,
}
impl<R: Runtime> Default for TauriSql<R> {
fn default() -> Self {
Self {
migrations: Some(Default::default()),
invoke_handler: Box::new(tauri::generate_handler![load, execute, select, close]),
}
}
}
impl<R: Runtime> TauriSql<R> {
/// Add migrations to a database.
#[must_use]
pub fn add_migrations(mut self, db_url: &str, migrations: Vec<Migration>) -> Self {
self.migrations
.as_mut()
.unwrap()
.insert(db_url.to_string(), MigrationList(migrations));
self
}
}
impl<R: Runtime> Plugin<R> for TauriSql<R> {
fn name(&self) -> &'static str {
"sql"
}
fn initialize(&mut self, app: &AppHandle<R>, config: serde_json::Value) -> PluginResult<()> {
tauri::async_runtime::block_on(async move {
let config: PluginConfig = if config.is_null() {
Default::default()
} else {
serde_json::from_value(config)?
};
#[cfg(feature = "sqlite")]
create_dir_all(app_path(app)).expect("problems creating App directory!");
let instances = DbInstances::default();
let mut lock = instances.0.lock().await;
for db in config.preload {
#[cfg(feature = "sqlite")]
let fqdb = path_mapper(app_path(app), &db);
#[cfg(not(feature = "sqlite"))]
let fqdb = db.clone();
if !Db::database_exists(&fqdb).await.unwrap_or(false) {
Db::create_database(&fqdb).await?;
}
let pool = Pool::connect(&fqdb).await?;
if let Some(migrations) = self.migrations.as_mut().unwrap().remove(&db) {
let migrator = Migrator::new(migrations).await?;
migrator.run(&pool).await?;
}
lock.insert(db, pool);
}
drop(lock);
app.manage(instances);
app.manage(Migrations(Mutex::new(self.migrations.take().unwrap())));
Ok(())
})
}
fn extend_api(&mut self, message: Invoke<R>) {
(self.invoke_handler)(message)
}
fn on_event(&mut self, app: &AppHandle<R>, event: &RunEvent) {
if let RunEvent::Exit = event {
tauri::async_runtime::block_on(async move {
let instances = &*app.state::<DbInstances>();
let instances = instances.0.lock().await;
for value in instances.values() {
value.close().await;
}
});
}
}
}
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "tauri-plugin-store"
version = "0.1.0"
edition.workspace = true
authors.workspace = true
license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde.workspace = true
serde_json.workspace = true
tauri.workspace = true
log.workspace = true
thiserror.workspace = true
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+177
View File
@@ -0,0 +1,177 @@
import { invoke } from '@tauri-apps/api/tauri';
import { appWindow } from '@tauri-apps/api/window';
// Copyright 2021 Tauri Programme within The Commons Conservancy
/**
* A key-value store persisted by the backend layer.
*/
class Store {
constructor(path) {
this.path = path;
}
/**
* Inserts a key-value pair into the store.
*
* @param key
* @param value
* @returns
*/
async set(key, value) {
await invoke("plugin:store|set", {
path: this.path,
key,
value,
});
}
/**
* Returns the value for the given `key` or `null` the key does not exist.
*
* @param key
* @returns
*/
async get(key) {
return await invoke("plugin:store|get", {
path: this.path,
key,
});
}
/**
* Returns `true` if the given `key` exists in the store.
*
* @param key
* @returns
*/
async has(key) {
return await invoke("plugin:store|has", {
path: this.path,
key,
});
}
/**
* Removes a key-value pair from the store.
*
* @param key
* @returns
*/
async delete(key) {
return await invoke("plugin:store|delete", {
path: this.path,
key,
});
}
/**
* Clears the store, removing all key-value pairs.
*
* Note: To clear the storage and reset it to it's `default` value, use `reset` instead.
* @returns
*/
async clear() {
return await invoke("plugin:store|clear", {
path: this.path,
});
}
/**
* Resets the store to it's `default` value.
*
* If no default value has been set, this method behaves identical to `clear`.
* @returns
*/
async reset() {
return await invoke("plugin:store|reset", {
path: this.path,
});
}
/**
* Returns a list of all key in the store.
*
* @returns
*/
async keys() {
return await invoke("plugin:store|keys", {
path: this.path,
});
}
/**
* Returns a list of all values in the store.
*
* @returns
*/
async values() {
return await invoke("plugin:store|values", {
path: this.path,
});
}
/**
* Returns a list of all entries in the store.
*
* @returns
*/
async entries() {
return await invoke("plugin:store|entries", {
path: this.path,
});
}
/**
* Returns the number of key-value pairs in the store.
*
* @returns
*/
async length() {
return await invoke("plugin:store|length", {
path: this.path,
});
}
/**
* Attempts to load the on-disk state at the stores `path` into memory.
*
* This method is useful if the on-disk state was edited by the user and you want to synchronize the changes.
*
* Note: This method does not emit change events.
* @returns
*/
async load() {
return await invoke("plugin:store|load", {
path: this.path,
});
}
/**
* Saves the store to disk at the stores `path`.
*
* As the store is only persistet to disk before the apps exit, changes might be lost in a crash.
* This method let's you persist the store to disk whenever you deem necessary.
* @returns
*/
async save() {
return await invoke("plugin:store|save", {
path: this.path,
});
}
/**
* Listen to changes on a store key.
* @param key
* @param cb
* @returns A promise resolving to a function to unlisten to the event.
*/
async onKeyChange(key, cb) {
return await appWindow.listen("store://change", (event) => {
if (event.payload.path === this.path && event.payload.key === key) {
cb(event.payload.value);
}
});
}
/**
* Listen to changes on the store.
* @param cb
* @returns A promise resolving to a function to unlisten to the event.
*/
async onChange(cb) {
return await appWindow.listen("store://change", (event) => {
if (event.payload.path === this.path) {
cb(event.payload.key, event.payload.value);
}
});
}
}
export { Store };
//# sourceMappingURL=index.mjs.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.mjs","sources":["../index.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
+210
View File
@@ -0,0 +1,210 @@
// Copyright 2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import { invoke } from "@tauri-apps/api/tauri";
import { UnlistenFn } from "@tauri-apps/api/event";
import { appWindow } from "@tauri-apps/api/window";
interface ChangePayload<T> {
path: string;
key: string;
value: T | null;
}
/**
* A key-value store persisted by the backend layer.
*/
export class Store {
path: string;
constructor(path: string) {
this.path = path;
}
/**
* Inserts a key-value pair into the store.
*
* @param key
* @param value
* @returns
*/
async set(key: string, value: unknown): Promise<void> {
await invoke("plugin:store|set", {
path: this.path,
key,
value,
});
}
/**
* Returns the value for the given `key` or `null` the key does not exist.
*
* @param key
* @returns
*/
async get<T>(key: string): Promise<T | null> {
return await invoke("plugin:store|get", {
path: this.path,
key,
});
}
/**
* Returns `true` if the given `key` exists in the store.
*
* @param key
* @returns
*/
async has(key: string): Promise<boolean> {
return await invoke("plugin:store|has", {
path: this.path,
key,
});
}
/**
* Removes a key-value pair from the store.
*
* @param key
* @returns
*/
async delete(key: string): Promise<boolean> {
return await invoke("plugin:store|delete", {
path: this.path,
key,
});
}
/**
* Clears the store, removing all key-value pairs.
*
* Note: To clear the storage and reset it to it's `default` value, use `reset` instead.
* @returns
*/
async clear(): Promise<void> {
return await invoke("plugin:store|clear", {
path: this.path,
});
}
/**
* Resets the store to it's `default` value.
*
* If no default value has been set, this method behaves identical to `clear`.
* @returns
*/
async reset(): Promise<void> {
return await invoke("plugin:store|reset", {
path: this.path,
});
}
/**
* Returns a list of all key in the store.
*
* @returns
*/
async keys(): Promise<string[]> {
return await invoke("plugin:store|keys", {
path: this.path,
});
}
/**
* Returns a list of all values in the store.
*
* @returns
*/
async values(): Promise<string[]> {
return await invoke("plugin:store|values", {
path: this.path,
});
}
/**
* Returns a list of all entries in the store.
*
* @returns
*/
async entries<T>(): Promise<Array<[key: string, value: T]>> {
return await invoke("plugin:store|entries", {
path: this.path,
});
}
/**
* Returns the number of key-value pairs in the store.
*
* @returns
*/
async length(): Promise<string[]> {
return await invoke("plugin:store|length", {
path: this.path,
});
}
/**
* Attempts to load the on-disk state at the stores `path` into memory.
*
* This method is useful if the on-disk state was edited by the user and you want to synchronize the changes.
*
* Note: This method does not emit change events.
* @returns
*/
async load(): Promise<void> {
return await invoke("plugin:store|load", {
path: this.path,
});
}
/**
* Saves the store to disk at the stores `path`.
*
* As the store is only persistet to disk before the apps exit, changes might be lost in a crash.
* This method let's you persist the store to disk whenever you deem necessary.
* @returns
*/
async save(): Promise<void> {
return await invoke("plugin:store|save", {
path: this.path,
});
}
/**
* Listen to changes on a store key.
* @param key
* @param cb
* @returns A promise resolving to a function to unlisten to the event.
*/
async onKeyChange<T>(
key: string,
cb: (value: T | null) => void
): Promise<UnlistenFn> {
return await appWindow.listen<ChangePayload<T>>(
"store://change",
(event) => {
if (event.payload.path === this.path && event.payload.key === key) {
cb(event.payload.value);
}
}
);
}
/**
* Listen to changes on the store.
* @param cb
* @returns A promise resolving to a function to unlisten to the event.
*/
async onChange(
cb: (key: string, value: unknown) => void
): Promise<UnlistenFn> {
return await appWindow.listen<ChangePayload<unknown>>(
"store://change",
(event) => {
if (event.payload.path === this.path) {
cb(event.payload.key, event.payload.value);
}
}
);
}
}
+29
View File
@@ -0,0 +1,29 @@
{
"name": "tauri-plugin-store-api",
"version": "0.0.0",
"license": "MIT or APACHE-2.0",
"type": "module",
"browser": "dist/index.min.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"browser": "./dist/index.min.js"
},
"scripts": {
"build": "rollup -c"
},
"files": [
"dist",
"!dist/**/*.map",
"README.md",
"LICENSE"
],
"devDependencies": {
"tslib": "^2.4.1"
},
"dependencies": {
"@tauri-apps/api": "^1.2.0"
}
}
+10
View File
@@ -0,0 +1,10 @@
import { readFileSync } from "fs";
import { createConfig } from "../../../shared/rollup.config.mjs";
export default createConfig({
pkg: JSON.parse(
readFileSync(new URL("./package.json", import.meta.url), "utf8")
),
external: [/^@tauri-apps\/api/],
});
+1
View File
@@ -0,0 +1 @@
../../../shared/tsconfig.json
+34
View File
@@ -0,0 +1,34 @@
// Copyright 2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::{Serialize, Serializer};
use std::path::PathBuf;
/// The error types.
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum Error {
#[error("Failed to serialize store. {0}")]
Serialize(Box<dyn std::error::Error>),
#[error("Failed to deserialize store. {0}")]
Deserialize(Box<dyn std::error::Error>),
/// JSON error.
#[error(transparent)]
Json(#[from] serde_json::Error),
/// IO error.
#[error(transparent)]
Io(#[from] std::io::Error),
/// Store not found
#[error("Store \"{0}\" not found")]
NotFound(PathBuf),
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
+370
View File
@@ -0,0 +1,370 @@
// Copyright 2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
pub use error::Error;
use log::warn;
use serde::Serialize;
use serde_json::Value as JsonValue;
use std::{collections::HashMap, path::PathBuf, sync::Mutex};
pub use store::{Store, StoreBuilder};
use tauri::{
plugin::{self, TauriPlugin},
AppHandle, Manager, RunEvent, Runtime, State, Window,
};
mod error;
mod store;
#[derive(Serialize, Clone)]
struct ChangePayload {
path: PathBuf,
key: String,
value: JsonValue,
}
#[derive(Default)]
struct StoreCollection {
stores: Mutex<HashMap<PathBuf, Store>>,
frozen: bool,
}
fn with_store<R: Runtime, T, F: FnOnce(&mut Store) -> Result<T, Error>>(
app: &AppHandle<R>,
collection: State<'_, StoreCollection>,
path: PathBuf,
f: F,
) -> Result<T, Error> {
let mut stores = collection.stores.lock().expect("mutex poisoned");
if !stores.contains_key(&path) {
if collection.frozen {
return Err(Error::NotFound(path));
}
let mut store = StoreBuilder::new(path.clone()).build();
// ignore loading errors, just use the default
if let Err(err) = store.load(app) {
warn!(
"Failed to load store {:?} from disk: {}. Falling back to default values.",
path, err
);
}
stores.insert(path.clone(), store);
}
f(stores
.get_mut(&path)
.expect("failed to retrieve store. This is a bug!"))
}
#[tauri::command]
async fn set<R: Runtime>(
app: AppHandle<R>,
window: Window<R>,
stores: State<'_, StoreCollection>,
path: PathBuf,
key: String,
value: JsonValue,
) -> Result<(), Error> {
with_store(&app, stores, path.clone(), |store| {
store.cache.insert(key.clone(), value.clone());
let _ = window.emit("store://change", ChangePayload { path, key, value });
Ok(())
})
}
#[tauri::command]
async fn get<R: Runtime>(
app: AppHandle<R>,
stores: State<'_, StoreCollection>,
path: PathBuf,
key: String,
) -> Result<Option<JsonValue>, Error> {
with_store(&app, stores, path, |store| {
Ok(store.cache.get(&key).cloned())
})
}
#[tauri::command]
async fn has<R: Runtime>(
app: AppHandle<R>,
stores: State<'_, StoreCollection>,
path: PathBuf,
key: String,
) -> Result<bool, Error> {
with_store(&app, stores, path, |store| {
Ok(store.cache.contains_key(&key))
})
}
#[tauri::command]
async fn delete<R: Runtime>(
app: AppHandle<R>,
window: Window<R>,
stores: State<'_, StoreCollection>,
path: PathBuf,
key: String,
) -> Result<bool, Error> {
with_store(&app, stores, path.clone(), |store| {
let flag = store.cache.remove(&key).is_some();
if flag {
let _ = window.emit(
"store://change",
ChangePayload {
path,
key,
value: JsonValue::Null,
},
);
}
Ok(flag)
})
}
#[tauri::command]
async fn clear<R: Runtime>(
app: AppHandle<R>,
window: Window<R>,
stores: State<'_, StoreCollection>,
path: PathBuf,
) -> Result<(), Error> {
with_store(&app, stores, path.clone(), |store| {
let keys = store.cache.keys().cloned().collect::<Vec<String>>();
store.cache.clear();
for key in keys {
let _ = window.emit(
"store://change",
ChangePayload {
path: path.clone(),
key,
value: JsonValue::Null,
},
);
}
Ok(())
})
}
#[tauri::command]
async fn reset<R: Runtime>(
app: AppHandle<R>,
window: Window<R>,
collection: State<'_, StoreCollection>,
path: PathBuf,
) -> Result<(), Error> {
let has_defaults = collection
.stores
.lock()
.expect("mutex poisoned")
.get(&path)
.map(|store| store.defaults.is_some());
if Some(true) == has_defaults {
with_store(&app, collection, path.clone(), |store| {
if let Some(defaults) = &store.defaults {
for (key, value) in &store.cache {
if defaults.get(key) != Some(value) {
let _ = window.emit(
"store://change",
ChangePayload {
path: path.clone(),
key: key.clone(),
value: defaults.get(key).cloned().unwrap_or(JsonValue::Null),
},
);
}
}
store.cache = defaults.clone();
}
Ok(())
})
} else {
clear(app, window, collection, path).await
}
}
#[tauri::command]
async fn keys<R: Runtime>(
app: AppHandle<R>,
stores: State<'_, StoreCollection>,
path: PathBuf,
) -> Result<Vec<String>, Error> {
with_store(&app, stores, path, |store| {
Ok(store.cache.keys().cloned().collect())
})
}
#[tauri::command]
async fn values<R: Runtime>(
app: AppHandle<R>,
stores: State<'_, StoreCollection>,
path: PathBuf,
) -> Result<Vec<JsonValue>, Error> {
with_store(&app, stores, path, |store| {
Ok(store.cache.values().cloned().collect())
})
}
#[tauri::command]
async fn entries<R: Runtime>(
app: AppHandle<R>,
stores: State<'_, StoreCollection>,
path: PathBuf,
) -> Result<Vec<(String, JsonValue)>, Error> {
with_store(&app, stores, path, |store| {
Ok(store.cache.clone().into_iter().collect())
})
}
#[tauri::command]
async fn length<R: Runtime>(
app: AppHandle<R>,
stores: State<'_, StoreCollection>,
path: PathBuf,
) -> Result<usize, Error> {
with_store(&app, stores, path, |store| Ok(store.cache.len()))
}
#[tauri::command]
async fn load<R: Runtime>(
app: AppHandle<R>,
stores: State<'_, StoreCollection>,
path: PathBuf,
) -> Result<(), Error> {
with_store(&app, stores, path, |store| store.load(&app))
}
#[tauri::command]
async fn save<R: Runtime>(
app: AppHandle<R>,
stores: State<'_, StoreCollection>,
path: PathBuf,
) -> Result<(), Error> {
with_store(&app, stores, path, |store| store.save(&app))
}
#[derive(Default)]
pub struct PluginBuilder {
stores: HashMap<PathBuf, Store>,
frozen: bool,
}
impl PluginBuilder {
/// Registers a store with the plugin.
///
/// # Examples
///
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// use tauri_plugin_store::{StoreBuilder,PluginBuilder};
///
/// let store = StoreBuilder::new("store.bin".parse()?).build();
///
/// let builder = PluginBuilder::default().store(store);
///
/// # Ok(())
/// # }
/// ```
pub fn store(mut self, store: Store) -> Self {
self.stores.insert(store.path.clone(), store);
self
}
/// Registers multiple stores with the plugin.
///
/// # Examples
///
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// use tauri_plugin_store::{StoreBuilder,PluginBuilder};
///
/// let store = StoreBuilder::new("store.bin".parse()?).build();
///
/// let builder = PluginBuilder::default().stores([store]);
///
/// # Ok(())
/// # }
/// ```
pub fn stores<T: IntoIterator<Item = Store>>(mut self, stores: T) -> Self {
self.stores = stores
.into_iter()
.map(|store| (store.path.clone(), store))
.collect();
self
}
/// Freezes the collection.
///
/// This causes requests for plugins that haven't been registered to fail
///
/// # Examples
///
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// use tauri_plugin_store::{StoreBuilder,PluginBuilder};
///
/// let store = StoreBuilder::new("store.bin".parse()?).build();
///
/// let builder = PluginBuilder::default().freeze();
///
/// # Ok(())
/// # }
/// ```
pub fn freeze(mut self) -> Self {
self.frozen = true;
self
}
/// Builds the plugin.
///
/// # Examples
///
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// use tauri_plugin_store::{StoreBuilder,PluginBuilder};
/// use tauri::Wry;
///
/// let store = StoreBuilder::new("store.bin".parse()?).build();
///
/// let plugin = PluginBuilder::default().build::<Wry>();
///
/// # Ok(())
/// # }
/// ```
pub fn build<R: Runtime>(mut self) -> TauriPlugin<R> {
plugin::Builder::new("store")
.invoke_handler(tauri::generate_handler![
set, get, has, delete, clear, reset, keys, values, length, entries, load, save
])
.setup(move |app_handle| {
for (path, store) in self.stores.iter_mut() {
// ignore loading errors, just use the default
if let Err(err) = store.load(app_handle) {
warn!(
"Failed to load store {:?} from disk: {}. Falling back to default values.",
path, err
);
}
}
app_handle.manage(StoreCollection {
stores: Mutex::new(self.stores),
frozen: self.frozen,
});
Ok(())
})
.on_event(|app_handle, event| {
if let RunEvent::Exit = event {
let collection = app_handle.state::<StoreCollection>();
for store in collection.stores.lock().expect("mutex poisoned").values() {
if let Err(err) = store.save(app_handle) {
eprintln!("failed to save store {:?} with error {:?}", store.path, err);
}
}
}
})
.build()
}
}
+212
View File
@@ -0,0 +1,212 @@
// Copyright 2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::Error;
use serde_json::Value as JsonValue;
use std::{
collections::HashMap,
fs::{create_dir_all, read, File},
io::Write,
path::PathBuf,
};
use tauri::{AppHandle, Runtime};
type SerializeFn = fn(&HashMap<String, JsonValue>) -> Result<Vec<u8>, Box<dyn std::error::Error>>;
type DeserializeFn = fn(&[u8]) -> Result<HashMap<String, JsonValue>, Box<dyn std::error::Error>>;
fn default_serialize(
cache: &HashMap<String, JsonValue>,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
Ok(serde_json::to_vec(&cache)?)
}
fn default_deserialize(
bytes: &[u8],
) -> Result<HashMap<String, JsonValue>, Box<dyn std::error::Error>> {
serde_json::from_slice(bytes).map_err(Into::into)
}
/// Builds a [`Store`]
pub struct StoreBuilder {
path: PathBuf,
defaults: Option<HashMap<String, JsonValue>>,
cache: HashMap<String, JsonValue>,
serialize: SerializeFn,
deserialize: DeserializeFn,
}
impl StoreBuilder {
/// Creates a new [`StoreBuilder`].
///
/// # Examples
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// use tauri_plugin_store::StoreBuilder;
///
/// let builder = StoreBuilder::new("store.bin".parse()?);
///
/// # Ok(())
/// # }
/// ```
pub fn new(path: PathBuf) -> Self {
Self {
path,
defaults: None,
cache: Default::default(),
serialize: default_serialize,
deserialize: default_deserialize,
}
}
/// Inserts a default key-value pair.
///
/// # Examples
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// use tauri_plugin_store::StoreBuilder;
/// use std::collections::HashMap;
///
/// let mut defaults = HashMap::new();
///
/// defaults.insert("foo".to_string(), "bar".into());
///
/// let builder = StoreBuilder::new("store.bin".parse()?)
/// .defaults(defaults);
///
/// # Ok(())
/// # }
pub fn defaults(mut self, defaults: HashMap<String, JsonValue>) -> Self {
self.cache = defaults.clone();
self.defaults = Some(defaults);
self
}
/// Inserts multiple key-value pairs.
///
/// # Examples
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// use tauri_plugin_store::StoreBuilder;
///
/// let builder = StoreBuilder::new("store.bin".parse()?)
/// .default("foo".to_string(), "bar".into());
///
/// # Ok(())
/// # }
pub fn default(mut self, key: String, value: JsonValue) -> Self {
self.cache.insert(key.clone(), value.clone());
self.defaults
.get_or_insert(HashMap::new())
.insert(key, value);
self
}
/// Defines a custom serialization function.
///
/// # Examples
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// use tauri_plugin_store::StoreBuilder;
///
/// let builder = StoreBuilder::new("store.json".parse()?)
/// .serialize(|cache| serde_json::to_vec(&cache).map_err(Into::into));
///
/// # Ok(())
/// # }
pub fn serialize(mut self, serialize: SerializeFn) -> Self {
self.serialize = serialize;
self
}
/// Defines a custom deserialization function
///
/// # Examples
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// use tauri_plugin_store::StoreBuilder;
///
/// let builder = StoreBuilder::new("store.json".parse()?)
/// .deserialize(|bytes| serde_json::from_slice(&bytes).map_err(Into::into));
///
/// # Ok(())
/// # }
pub fn deserialize(mut self, deserialize: DeserializeFn) -> Self {
self.deserialize = deserialize;
self
}
/// Builds the [`Store`].
///
/// # Examples
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// use tauri_plugin_store::StoreBuilder;
///
/// let store = StoreBuilder::new("store.bin".parse()?).build();
///
/// # Ok(())
/// # }
pub fn build(self) -> Store {
Store {
path: self.path,
defaults: self.defaults,
cache: self.cache,
serialize: self.serialize,
deserialize: self.deserialize,
}
}
}
#[derive(Clone)]
pub struct Store {
pub(crate) path: PathBuf,
pub(crate) defaults: Option<HashMap<String, JsonValue>>,
pub(crate) cache: HashMap<String, JsonValue>,
serialize: SerializeFn,
deserialize: DeserializeFn,
}
impl Store {
/// Update the store from the on-disk state
pub fn load<R: Runtime>(&mut self, app: &AppHandle<R>) -> Result<(), Error> {
let app_dir = app
.path_resolver()
.app_dir()
.expect("failed to resolve app dir");
let store_path = app_dir.join(&self.path);
let bytes = read(&store_path)?;
self.cache = (self.deserialize)(&bytes).map_err(Error::Deserialize)?;
Ok(())
}
/// Saves the store to disk
pub fn save<R: Runtime>(&self, app: &AppHandle<R>) -> Result<(), Error> {
let app_dir = app
.path_resolver()
.app_dir()
.expect("failed to resolve app dir");
let store_path = app_dir.join(&self.path);
create_dir_all(store_path.parent().expect("invalid store path"))?;
let bytes = (self.serialize)(&self.cache).map_err(Error::Serialize)?;
let mut f = File::create(&store_path)?;
f.write_all(&bytes)?;
Ok(())
}
}
impl std::fmt::Debug for Store {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Store")
.field("path", &self.path)
.field("defaults", &self.defaults)
.field("cache", &self.cache)
.finish()
}
}
+23
View File
@@ -0,0 +1,23 @@
[package]
name = "tauri-plugin-stronghold"
version = "0.1.0"
edition.workspace = true
authors.workspace = true
license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde.workspace = true
serde_json.workspace = true
tauri.workspace = true
log.workspace = true
thiserror.workspace = true
iota_stronghold = { version = "0.8" }
iota-crypto = "0.14"
hex = "0.4"
zeroize = { version = "1", features = ["zeroize_derive"] }
[dev-dependencies]
rand = "0.8"
rusty-fork = "0.3"
+217
View File
@@ -0,0 +1,217 @@
var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumerable:!0});};
var f={};e(f,{convertFileSrc:()=>w,invoke:()=>c,transformCallback:()=>s});function u(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function s(e,r=!1){let n=u(),t=`_${n}`;return Object.defineProperty(window,t,{value:o=>(r&&Reflect.deleteProperty(window,t),e==null?void 0:e(o)),writable:!1,configurable:!0}),n}async function c(e,r={}){return new Promise((n,t)=>{let o=s(i=>{n(i),Reflect.deleteProperty(window,`_${a}`);},!0),a=s(i=>{t(i),Reflect.deleteProperty(window,`_${o}`);},!0);window.__TAURI_IPC__({cmd:e,callback:o,error:a,...r});})}function w(e,r="asset"){let n=encodeURIComponent(e);return navigator.userAgent.includes("Windows")?`https://${r}.localhost/${n}`:`${r}://localhost/${n}`}
function toBytesDto(v) {
if (typeof v === "string") {
return v;
}
return Array.from(v instanceof ArrayBuffer ? new Uint8Array(v) : v);
}
class Location {
constructor(type, payload) {
this.type = type;
this.payload = payload;
}
static generic(vault, record) {
return new Location("Generic", {
vault: toBytesDto(vault),
record: toBytesDto(record),
});
}
static counter(vault, counter) {
return new Location("Counter", {
vault: toBytesDto(vault),
counter,
});
}
}
class ProcedureExecutor {
constructor(procedureArgs) {
this.procedureArgs = procedureArgs;
}
async generateSLIP10Seed(outputLocation, sizeBytes) {
return await c("plugin:stronghold|execute_procedure", {
...this.procedureArgs,
procedure: {
type: "SLIP10Generate",
payload: {
output: outputLocation,
sizeBytes,
},
},
}).then((n) => Uint8Array.from(n));
}
async deriveSLIP10(chain, source, sourceLocation, outputLocation) {
return await c("plugin:stronghold|execute_procedure", {
...this.procedureArgs,
procedure: {
type: "SLIP10Derive",
payload: {
chain,
input: {
type: source,
payload: sourceLocation,
},
output: outputLocation,
},
},
}).then((n) => Uint8Array.from(n));
}
async recoverBIP39(mnemonic, outputLocation, passphrase) {
return await c("plugin:stronghold|execute_procedure", {
...this.procedureArgs,
procedure: {
type: "BIP39Recover",
payload: {
mnemonic,
passphrase,
output: outputLocation,
},
},
}).then((n) => Uint8Array.from(n));
}
async generateBIP39(outputLocation, passphrase) {
return await c("plugin:stronghold|execute_procedure", {
...this.procedureArgs,
procedure: {
type: "BIP39Generate",
payload: {
output: outputLocation,
passphrase,
},
},
}).then((n) => Uint8Array.from(n));
}
async getEd25519PublicKey(privateKeyLocation) {
return await c("plugin:stronghold|execute_procedure", {
...this.procedureArgs,
procedure: {
type: "PublicKey",
payload: {
type: "Ed25519",
privateKey: privateKeyLocation,
},
},
}).then((n) => Uint8Array.from(n));
}
async signEd25519(privateKeyLocation, msg) {
return await c("plugin:stronghold|execute_procedure", {
...this.procedureArgs,
procedure: {
type: "Ed25519Sign",
payload: {
privateKey: privateKeyLocation,
msg,
},
},
}).then((n) => Uint8Array.from(n));
}
}
class Client {
constructor(path, name) {
this.path = path;
this.name = toBytesDto(name);
}
getVault(name) {
return new Vault(this.path, this.name, toBytesDto(name));
}
getStore() {
return new Store(this.path, this.name);
}
}
class Store {
constructor(path, client) {
this.path = path;
this.client = client;
}
async get(key) {
return await c("plugin:stronghold|get_store_record", {
snapshotPath: this.path,
client: this.client,
key: toBytesDto(key),
}).then((v) => Uint8Array.from(v));
}
async insert(key, value, lifetime) {
return await c("plugin:stronghold|save_store_record", {
snapshotPath: this.path,
client: this.client,
key: toBytesDto(key),
value,
lifetime,
});
}
async remove(key) {
return await c("plugin:stronghold|remove_store_record", {
snapshotPath: this.path,
client: this.client,
key: toBytesDto(key),
}).then((v) => (v != null ? Uint8Array.from(v) : null));
}
}
class Vault extends ProcedureExecutor {
constructor(path, client, name) {
super({
snapshotPath: path,
client,
vault: name,
});
this.path = path;
this.client = toBytesDto(client);
this.name = toBytesDto(name);
}
async insert(recordPath, secret) {
return await c("plugin:stronghold|save_secret", {
snapshotPath: this.path,
client: this.client,
vault: this.name,
recordPath: toBytesDto(recordPath),
secret,
});
}
async remove(location) {
return await c("plugin:stronghold|remove_secret", {
snapshotPath: this.path,
client: this.client,
vault: this.name,
location,
});
}
}
class Stronghold {
constructor(path, password) {
this.path = path;
void this.reload(password);
}
async reload(password) {
return await c("plugin:stronghold|initialize", {
snapshotPath: this.path,
password,
});
}
async unload() {
return await c("plugin:stronghold|destroy", {
snapshotPath: this.path,
});
}
async loadClient(client) {
return await c("plugin:stronghold|load_client", {
snapshotPath: this.path,
client: toBytesDto(client),
}).then(() => new Client(this.path, client));
}
async createClient(client) {
return await c("plugin:stronghold|create_client", {
snapshotPath: this.path,
client: toBytesDto(client),
}).then(() => new Client(this.path, client));
}
async save() {
return await c("plugin:stronghold|save", {
snapshotPath: this.path,
});
}
}
export { Client, Location, Store, Stronghold, Vault };
//# sourceMappingURL=index.min.js.map
File diff suppressed because one or more lines are too long
+215
View File
@@ -0,0 +1,215 @@
import { invoke } from '@tauri-apps/api/tauri';
function toBytesDto(v) {
if (typeof v === "string") {
return v;
}
return Array.from(v instanceof ArrayBuffer ? new Uint8Array(v) : v);
}
class Location {
constructor(type, payload) {
this.type = type;
this.payload = payload;
}
static generic(vault, record) {
return new Location("Generic", {
vault: toBytesDto(vault),
record: toBytesDto(record),
});
}
static counter(vault, counter) {
return new Location("Counter", {
vault: toBytesDto(vault),
counter,
});
}
}
class ProcedureExecutor {
constructor(procedureArgs) {
this.procedureArgs = procedureArgs;
}
async generateSLIP10Seed(outputLocation, sizeBytes) {
return await invoke("plugin:stronghold|execute_procedure", {
...this.procedureArgs,
procedure: {
type: "SLIP10Generate",
payload: {
output: outputLocation,
sizeBytes,
},
},
}).then((n) => Uint8Array.from(n));
}
async deriveSLIP10(chain, source, sourceLocation, outputLocation) {
return await invoke("plugin:stronghold|execute_procedure", {
...this.procedureArgs,
procedure: {
type: "SLIP10Derive",
payload: {
chain,
input: {
type: source,
payload: sourceLocation,
},
output: outputLocation,
},
},
}).then((n) => Uint8Array.from(n));
}
async recoverBIP39(mnemonic, outputLocation, passphrase) {
return await invoke("plugin:stronghold|execute_procedure", {
...this.procedureArgs,
procedure: {
type: "BIP39Recover",
payload: {
mnemonic,
passphrase,
output: outputLocation,
},
},
}).then((n) => Uint8Array.from(n));
}
async generateBIP39(outputLocation, passphrase) {
return await invoke("plugin:stronghold|execute_procedure", {
...this.procedureArgs,
procedure: {
type: "BIP39Generate",
payload: {
output: outputLocation,
passphrase,
},
},
}).then((n) => Uint8Array.from(n));
}
async getEd25519PublicKey(privateKeyLocation) {
return await invoke("plugin:stronghold|execute_procedure", {
...this.procedureArgs,
procedure: {
type: "PublicKey",
payload: {
type: "Ed25519",
privateKey: privateKeyLocation,
},
},
}).then((n) => Uint8Array.from(n));
}
async signEd25519(privateKeyLocation, msg) {
return await invoke("plugin:stronghold|execute_procedure", {
...this.procedureArgs,
procedure: {
type: "Ed25519Sign",
payload: {
privateKey: privateKeyLocation,
msg,
},
},
}).then((n) => Uint8Array.from(n));
}
}
class Client {
constructor(path, name) {
this.path = path;
this.name = toBytesDto(name);
}
getVault(name) {
return new Vault(this.path, this.name, toBytesDto(name));
}
getStore() {
return new Store(this.path, this.name);
}
}
class Store {
constructor(path, client) {
this.path = path;
this.client = client;
}
async get(key) {
return await invoke("plugin:stronghold|get_store_record", {
snapshotPath: this.path,
client: this.client,
key: toBytesDto(key),
}).then((v) => Uint8Array.from(v));
}
async insert(key, value, lifetime) {
return await invoke("plugin:stronghold|save_store_record", {
snapshotPath: this.path,
client: this.client,
key: toBytesDto(key),
value,
lifetime,
});
}
async remove(key) {
return await invoke("plugin:stronghold|remove_store_record", {
snapshotPath: this.path,
client: this.client,
key: toBytesDto(key),
}).then((v) => (v != null ? Uint8Array.from(v) : null));
}
}
class Vault extends ProcedureExecutor {
constructor(path, client, name) {
super({
snapshotPath: path,
client,
vault: name,
});
this.path = path;
this.client = toBytesDto(client);
this.name = toBytesDto(name);
}
async insert(recordPath, secret) {
return await invoke("plugin:stronghold|save_secret", {
snapshotPath: this.path,
client: this.client,
vault: this.name,
recordPath: toBytesDto(recordPath),
secret,
});
}
async remove(location) {
return await invoke("plugin:stronghold|remove_secret", {
snapshotPath: this.path,
client: this.client,
vault: this.name,
location,
});
}
}
class Stronghold {
constructor(path, password) {
this.path = path;
void this.reload(password);
}
async reload(password) {
return await invoke("plugin:stronghold|initialize", {
snapshotPath: this.path,
password,
});
}
async unload() {
return await invoke("plugin:stronghold|destroy", {
snapshotPath: this.path,
});
}
async loadClient(client) {
return await invoke("plugin:stronghold|load_client", {
snapshotPath: this.path,
client: toBytesDto(client),
}).then(() => new Client(this.path, client));
}
async createClient(client) {
return await invoke("plugin:stronghold|create_client", {
snapshotPath: this.path,
client: toBytesDto(client),
}).then(() => new Client(this.path, client));
}
async save() {
return await invoke("plugin:stronghold|save", {
snapshotPath: this.path,
});
}
}
export { Client, Location, Store, Stronghold, Vault };
//# sourceMappingURL=index.mjs.map
File diff suppressed because one or more lines are too long
+355
View File
@@ -0,0 +1,355 @@
import { invoke } from "@tauri-apps/api/tauri";
type BytesDto = string | number[];
export type ClientPath =
| string
| Iterable<number>
| ArrayLike<number>
| ArrayBuffer;
export type VaultPath =
| string
| Iterable<number>
| ArrayLike<number>
| ArrayBuffer;
export type RecordPath =
| string
| Iterable<number>
| ArrayLike<number>
| ArrayBuffer;
export type StoreKey =
| string
| Iterable<number>
| ArrayLike<number>
| ArrayBuffer;
function toBytesDto(
v: ClientPath | VaultPath | RecordPath | StoreKey
): string | number[] {
if (typeof v === "string") {
return v;
}
return Array.from(v instanceof ArrayBuffer ? new Uint8Array(v) : v);
}
export interface ConnectionLimits {
maxPendingIncoming?: number;
maxPendingOutgoing?: number;
maxEstablishedIncoming?: number;
maxEstablishedOutgoing?: number;
maxEstablishedPerPeer?: number;
maxEstablishedTotal?: number;
}
export interface PeerAddress {
known: string[]; // multiaddr
use_relay_fallback: boolean;
}
export interface AddressInfo {
peers: Map<string, PeerAddress>;
relays: string[]; // peers
}
export interface ClientAccess {
useVaultDefault?: boolean;
useVaultExceptions?: Map<VaultPath, boolean>;
writeVaultDefault?: boolean;
writeVaultExceptions?: Map<VaultPath, boolean>;
cloneVaultDefault?: boolean;
cloneVaultExceptions?: Map<VaultPath, boolean>;
readStore?: boolean;
writeStore?: boolean;
}
export interface Permissions {
default?: ClientAccess;
exceptions?: Map<VaultPath, ClientAccess>;
}
export interface NetworkConfig {
requestTimeout?: Duration;
connectionTimeout?: Duration;
connectionsLimit?: ConnectionLimits;
enableMdns?: boolean;
enableRelay?: boolean;
addresses?: AddressInfo;
peerPermissions?: Map<string, Permissions>;
permissionsDefault?: Permissions;
}
export interface Duration {
millis: number;
nanos: number;
}
export class Location {
type: string;
payload: Record<string, unknown>;
constructor(type: string, payload: Record<string, unknown>) {
this.type = type;
this.payload = payload;
}
static generic(vault: VaultPath, record: RecordPath): Location {
return new Location("Generic", {
vault: toBytesDto(vault),
record: toBytesDto(record),
});
}
static counter(vault: VaultPath, counter: number): Location {
return new Location("Counter", {
vault: toBytesDto(vault),
counter,
});
}
}
class ProcedureExecutor {
procedureArgs: Record<string, unknown>;
constructor(procedureArgs: Record<string, unknown>) {
this.procedureArgs = procedureArgs;
}
async generateSLIP10Seed(
outputLocation: Location,
sizeBytes?: number
): Promise<Uint8Array> {
return await invoke<number[]>("plugin:stronghold|execute_procedure", {
...this.procedureArgs,
procedure: {
type: "SLIP10Generate",
payload: {
output: outputLocation,
sizeBytes,
},
},
}).then((n) => Uint8Array.from(n));
}
async deriveSLIP10(
chain: number[],
source: "Seed" | "Key",
sourceLocation: Location,
outputLocation: Location
): Promise<Uint8Array> {
return await invoke<number[]>("plugin:stronghold|execute_procedure", {
...this.procedureArgs,
procedure: {
type: "SLIP10Derive",
payload: {
chain,
input: {
type: source,
payload: sourceLocation,
},
output: outputLocation,
},
},
}).then((n) => Uint8Array.from(n));
}
async recoverBIP39(
mnemonic: string,
outputLocation: Location,
passphrase?: string
): Promise<Uint8Array> {
return await invoke<number[]>("plugin:stronghold|execute_procedure", {
...this.procedureArgs,
procedure: {
type: "BIP39Recover",
payload: {
mnemonic,
passphrase,
output: outputLocation,
},
},
}).then((n) => Uint8Array.from(n));
}
async generateBIP39(
outputLocation: Location,
passphrase?: string
): Promise<Uint8Array> {
return await invoke<number[]>("plugin:stronghold|execute_procedure", {
...this.procedureArgs,
procedure: {
type: "BIP39Generate",
payload: {
output: outputLocation,
passphrase,
},
},
}).then((n) => Uint8Array.from(n));
}
async getEd25519PublicKey(privateKeyLocation: Location): Promise<Uint8Array> {
return await invoke<number[]>("plugin:stronghold|execute_procedure", {
...this.procedureArgs,
procedure: {
type: "PublicKey",
payload: {
type: "Ed25519",
privateKey: privateKeyLocation,
},
},
}).then((n) => Uint8Array.from(n));
}
async signEd25519(
privateKeyLocation: Location,
msg: string
): Promise<Uint8Array> {
return await invoke<number[]>("plugin:stronghold|execute_procedure", {
...this.procedureArgs,
procedure: {
type: "Ed25519Sign",
payload: {
privateKey: privateKeyLocation,
msg,
},
},
}).then((n) => Uint8Array.from(n));
}
}
export class Client {
path: string;
name: BytesDto;
constructor(path: string, name: ClientPath) {
this.path = path;
this.name = toBytesDto(name);
}
getVault(name: VaultPath): Vault {
return new Vault(this.path, this.name, toBytesDto(name));
}
getStore(): Store {
return new Store(this.path, this.name);
}
}
export class Store {
path: string;
client: BytesDto;
constructor(path: string, client: BytesDto) {
this.path = path;
this.client = client;
}
async get(key: StoreKey): Promise<Uint8Array> {
return await invoke<number[]>("plugin:stronghold|get_store_record", {
snapshotPath: this.path,
client: this.client,
key: toBytesDto(key),
}).then((v) => Uint8Array.from(v));
}
async insert(
key: StoreKey,
value: number[],
lifetime?: Duration
): Promise<void> {
return await invoke("plugin:stronghold|save_store_record", {
snapshotPath: this.path,
client: this.client,
key: toBytesDto(key),
value,
lifetime,
});
}
async remove(key: StoreKey): Promise<Uint8Array | null> {
return await invoke<number[] | null>(
"plugin:stronghold|remove_store_record",
{
snapshotPath: this.path,
client: this.client,
key: toBytesDto(key),
}
).then((v) => (v != null ? Uint8Array.from(v) : null));
}
}
export class Vault extends ProcedureExecutor {
path: string;
client: BytesDto;
name: BytesDto;
constructor(path: string, client: ClientPath, name: VaultPath) {
super({
snapshotPath: path,
client,
vault: name,
});
this.path = path;
this.client = toBytesDto(client);
this.name = toBytesDto(name);
}
async insert(recordPath: RecordPath, secret: number[]): Promise<void> {
return await invoke("plugin:stronghold|save_secret", {
snapshotPath: this.path,
client: this.client,
vault: this.name,
recordPath: toBytesDto(recordPath),
secret,
});
}
async remove(location: Location): Promise<void> {
return await invoke("plugin:stronghold|remove_secret", {
snapshotPath: this.path,
client: this.client,
vault: this.name,
location,
});
}
}
export class Stronghold {
path: string;
constructor(path: string, password: string) {
this.path = path;
void this.reload(password);
}
private async reload(password: string): Promise<void> {
return await invoke("plugin:stronghold|initialize", {
snapshotPath: this.path,
password,
});
}
async unload(): Promise<void> {
return await invoke("plugin:stronghold|destroy", {
snapshotPath: this.path,
});
}
async loadClient(client: ClientPath): Promise<Client> {
return await invoke("plugin:stronghold|load_client", {
snapshotPath: this.path,
client: toBytesDto(client),
}).then(() => new Client(this.path, client));
}
async createClient(client: ClientPath): Promise<Client> {
return await invoke("plugin:stronghold|create_client", {
snapshotPath: this.path,
client: toBytesDto(client),
}).then(() => new Client(this.path, client));
}
async save(): Promise<void> {
return await invoke("plugin:stronghold|save", {
snapshotPath: this.path,
});
}
}
+29
View File
@@ -0,0 +1,29 @@
{
"name": "tauri-plugin-stronghold-api",
"version": "0.0.0",
"license": "MIT or APACHE-2.0",
"type": "module",
"browser": "dist/index.min.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"browser": "./dist/index.min.js"
},
"scripts": {
"build": "rollup -c"
},
"files": [
"dist",
"!dist/**/*.map",
"README.md",
"LICENSE"
],
"devDependencies": {
"tslib": "^2.4.1"
},
"dependencies": {
"@tauri-apps/api": "^1.2.0"
}
}
@@ -0,0 +1,10 @@
import { readFileSync } from "fs";
import { createConfig } from "../../../shared/rollup.config.mjs";
export default createConfig({
pkg: JSON.parse(
readFileSync(new URL("./package.json", import.meta.url), "utf8")
),
external: [/^@tauri-apps\/api/],
});
+1
View File
@@ -0,0 +1 @@
../../../shared/tsconfig.json

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