Merge branch 'dev' into next

pull/257/head
FabianLars 2 years ago
commit 082e05e045
No known key found for this signature in database
GPG Key ID: 3B12BC1DEBF61125

598
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -14,4 +14,4 @@ thiserror = "1"
edition = "2021"
authors = [ "Tauri Programme within The Commons Conservancy" ]
license = "Apache-2.0 OR MIT"
rust-version = "1.59"
rust-version = "1.64"

@ -7,12 +7,15 @@
| [fs-extra](plugins/fs-extra) | File system methods that aren't included in the core API. | ✅ | ✅ | ✅ | ? | ? |
| [fs-watch](plugins/fs-watch) | Watch the filesystem for changes. | ✅ | ✅ | ✅ | ? | ? |
| [localhost](plugins/localhost) | Use a localhost server in production apps. | ✅ | ✅ | ✅ | ? | ? |
| [log](plugins/log) | Configurable logging. | ✅ | ✅ | ✅ | ? | ? |
| [log](plugins/log) | Configurable logging. | ✅ | ✅ | ✅ | ✅ | ✅ |
| [persisted-scope](plugins/persisted-scope) | Persist runtime scope changes on the filesystem. | ✅ | ✅ | ✅ | ? | ? |
| [positioner](plugins/positioner) | Move windows to common locations. | ✅ | ✅ | ✅ | ? | ? |
| [single-instance](plugins/single-instance) | Ensure a single instance of your tauri app is running. | ✅ | ? | ✅ | ? | ? |
| [sql](plugins/sql) | Interface with SQL databases. | ✅ | ✅ | ✅ | ? | ? |
| [store](plugins/store) | Persistent key value storage. | ✅ | ✅ | ✅ | ? | ? |
| [stronghold](plugins/stronghold) | Encrypted, secure database. | ✅ | ✅ | ✅ | ? | ? |
| [upload](plugins/upload) | Tauri plugin for file uploads through HTTP. | ✅ | ✅ | ✅ | ? | ? |
| [websocket](plugins/websocket) | | ✅ | ✅ | ✅ | ? | ? |
| [websocket](plugins/websocket) | Open a WebSocket connection using a Rust client in JS. | ✅ | ✅ | ✅ | ? | ? |
| [window-state](plugins/window-state) | Persist window sizes and positions. | ✅ | ✅ | ✅ | ? | ? |
_This repo and all plugins require a Rust version of at least **1.64**_

@ -11,13 +11,13 @@
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-terser": "^0.3.0",
"@rollup/plugin-terser": "^0.4.0",
"@rollup/plugin-typescript": "^11.0.0",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.46.1",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.5.0",
"eslint-config-standard-with-typescript": "^27.0.0",
"eslint-config-standard-with-typescript": "^34.0.0",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-n": "^15.0.0",
"eslint-plugin-promise": "^6.0.0",

@ -4,6 +4,8 @@ Use hardware security-keys in your Tauri App.
## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

@ -154,7 +154,7 @@ pub fn sign(
let sig = encode_config(sign_data, URL_SAFE_NO_PAD);
println!("Sign result: {}", sig);
println!("Sign result: {sig}");
println!(
"Key handle used: {}",
encode_config(&handle_used, URL_SAFE_NO_PAD)
@ -173,10 +173,8 @@ pub fn sign(
}
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 d =
format!(r#"{{"challenge": "{challenge}", "version": "U2F_V2", "appId": "{application}"}}"#);
let mut challenge = Sha256::new();
challenge.update(d.as_bytes());
let chall_bytes = challenge.finalize().to_vec();

@ -16,7 +16,7 @@ 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),
timestamp: format!("{utc:?}"),
app_id: app_id.to_string(),
}
}

@ -4,6 +4,8 @@ Automatically launch your application at startup. Supports Windows, Mac (via App
## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

@ -3,6 +3,7 @@
// SPDX-License-Identifier: MIT
use auto_launch::{AutoLaunch, AutoLaunchBuilder};
use log::info;
use serde::{ser::Serializer, Serialize};
use tauri::{
command,
@ -98,7 +99,6 @@ pub fn init<R: Runtime>(
.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);
@ -110,7 +110,26 @@ pub fn init<R: Runtime>(
#[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());
{
// on macOS, current_exe gives path to /Applications/Example.app/MacOS/Example
// but this results in seeing a Unix Executable in macOS login items
// It must be: /Applications/Example.app
// If it didn't find exactly a single occurance of .app, it will default to
// exe path to not break it.
let exe_path = current_exe.canonicalize()?.display().to_string();
let parts: Vec<&str> = exe_path.split(".app/").collect();
let app_path = if parts.len() == 2 {
format!(
"{}{}",
parts.get(0).unwrap().to_string(),
".app"
)
} else {
exe_path
};
info!("auto_start path {}", &app_path);
builder.set_app_path(&app_path);
}
#[cfg(target_os = "linux")]
if let Some(appimage) = app
.env()

@ -4,6 +4,8 @@ Additional file system methods not included in the core API.
## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

@ -15,4 +15,5 @@ serde_json.workspace = true
tauri.workspace = true
log.workspace = true
thiserror.workspace = true
notify = "4.0"
notify = { version = "5" , features = ["serde"] }
notify-debouncer-mini = { version = "0.2.1" , features = ["serde"] }

@ -4,6 +4,8 @@ Watch files and directories for changes using [notify](https://github.com/notify
## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

@ -12,22 +12,31 @@ export interface DebouncedWatchOptions extends WatchOptions {
delayMs?: number;
}
export interface RawEvent {
path: string | null;
operation: number;
cookie: number | null;
}
export type RawEvent = {
type: RawEventKind;
paths: string[];
attrs: unknown;
};
type RawEventKind =
| "any "
| {
access?: unknown;
}
| {
create?: unknown;
}
| {
modify?: unknown;
}
| {
remove?: unknown;
}
| "other";
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 } };
| { kind: "any"; path: string }
| { kind: "AnyContinous"; path: string };
async function unwatch(id: number): Promise<void> {
await invoke("plugin:fs-watch|unwatch", { id });

@ -1,7 +1,5 @@
use notify::{
raw_watcher, watcher, DebouncedEvent, Op, RawEvent, RecommendedWatcher, RecursiveMode,
Watcher as _,
};
use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};
use notify_debouncer_mini::{new_debouncer, DebounceEventResult, Debouncer};
use serde::{ser::Serializer, Deserialize, Serialize};
use tauri::{
command,
@ -39,72 +37,33 @@ impl Serialize for Error {
}
#[derive(Default)]
struct WatcherCollection(Mutex<HashMap<Id, (RecommendedWatcher, Vec<PathBuf>)>>);
struct WatcherCollection(Mutex<HashMap<Id, (WatcherKind, Vec<PathBuf>)>>);
#[derive(Clone, Serialize)]
struct RawEventWrapper {
path: Option<PathBuf>,
operation: u32,
cookie: Option<u32>,
enum WatcherKind {
Debouncer(Debouncer<RecommendedWatcher>),
Watcher(RecommendedWatcher),
}
#[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) {
fn watch_raw<R: Runtime>(window: Window<R>, rx: Receiver<notify::Result<Event>>, id: Id) {
spawn(move || {
let event_name = format!("watcher://raw-event/{}", id);
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,
},
);
if let Ok(event) = event {
// TODO: Should errors be emitted too?
let _ = window.emit(&event_name, event);
}
}
});
}
fn watch_debounced<R: Runtime>(window: Window<R>, rx: Receiver<DebouncedEvent>, id: Id) {
fn watch_debounced<R: Runtime>(window: Window<R>, rx: Receiver<DebounceEventResult>, id: Id) {
spawn(move || {
let event_name = format!("watcher://debounced-event/{}", id);
let event_name = format!("watcher://debounced-event/{id}");
while let Ok(event) = rx.recv() {
let _ = window.emit(&event_name, DebouncedEventWrapper::from(event));
if let Ok(event) = event {
// TODO: Should errors be emitted too?
let _ = window.emit(&event_name, event);
}
}
});
}
@ -132,20 +91,21 @@ async fn watch<R: Runtime>(
let watcher = if let Some(delay) = options.delay_ms {
let (tx, rx) = channel();
let mut watcher = watcher(tx, Duration::from_millis(delay))?;
let mut debouncer = new_debouncer(Duration::from_millis(delay), None, tx)?;
let watcher = debouncer.watcher();
for path in &paths {
watcher.watch(path, mode)?;
}
watch_debounced(window, rx, id);
watcher
WatcherKind::Debouncer(debouncer)
} else {
let (tx, rx) = channel();
let mut watcher = raw_watcher(tx)?;
let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
for path in &paths {
watcher.watch(path, mode)?;
}
watch_raw(window, rx, id);
watcher
WatcherKind::Watcher(watcher)
};
watchers.0.lock().unwrap().insert(id, (watcher, paths));
@ -155,10 +115,19 @@ async fn watch<R: Runtime>(
#[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)?;
}
if let Some((watcher, paths)) = watchers.0.lock().unwrap().remove(&id) {
match watcher {
WatcherKind::Debouncer(mut debouncer) => {
for path in paths {
debouncer.watcher().unwatch(&path)?
}
}
WatcherKind::Watcher(mut watcher) => {
for path in paths {
watcher.unwatch(&path)?
}
}
};
}
Ok(())
}

@ -6,6 +6,8 @@ Expose your apps assets through a localhost server instead of the default custom
## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

@ -62,8 +62,8 @@ impl Builder {
.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");
let server =
Server::http(&format!("localhost:{port}")).expect("Unable to spawn server");
for req in server.incoming_requests() {
let path = req
.url()

@ -4,6 +4,8 @@ Configurable logging for your Tauri app.
## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

@ -351,7 +351,7 @@ fn get_log_file_path(
rotation_strategy: &RotationStrategy,
max_file_size: u128,
) -> plugin::Result<PathBuf> {
let path = dir.as_ref().join(format!("{}.log", app_name));
let path = dir.as_ref().join(format!("{app_name}.log"));
if path.exists() {
let log_size = File::open(&path)?.metadata()?.len() as u128;

@ -4,6 +4,8 @@ Save filesystem and asset scopes and restore them when the app is reopened.
## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

@ -6,6 +6,8 @@ This plugin is a port of [electron-positioner](https://github.com/jenslind/elect
## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

@ -4,6 +4,8 @@ Ensure a single instance of your tauri app is running.
## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

@ -38,8 +38,8 @@ pub fn init<R: Runtime>(f: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> {
callback: f,
app_handle: app.clone(),
};
let dbus_name = format!("org.{}.SingleInstance", id);
let dbus_path = format!("/org/{}/SingleInstance", id);
let dbus_name = format!("org.{id}.SingleInstance");
let dbus_path = format!("/org/{id}/SingleInstance");
match ConnectionBuilder::session()
.unwrap()

@ -31,9 +31,9 @@ pub fn init<R: Runtime>(f: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> {
.setup(|app| {
let id = &app.config().tauri.bundle.identifier;
let class_name = encode_wide(format!("{}-sic", id));
let window_name = encode_wide(format!("{}-siw", id));
let mutex_name = encode_wide(format!("{}-sim", id));
let class_name = encode_wide(format!("{id}-sic"));
let window_name = encode_wide(format!("{id}-siw"));
let mutex_name = encode_wide(format!("{id}-sim"));
let hmutex =
unsafe { CreateMutexW(std::ptr::null(), true.into(), mutex_name.as_ptr()) };
@ -113,10 +113,10 @@ unsafe extern "system" fn single_instance_window_proc<R: Runtime>(
let cds_ptr = lparam as *const COPYDATASTRUCT;
if (*cds_ptr).dwData == WMCOPYDATA_SINGLE_INSTANCE_DATA {
let data = CStr::from_ptr((*cds_ptr).lpData as _).to_string_lossy();
let mut s = data.split("|");
let mut s = data.split('|');
let cwd = s.next().unwrap();
let args = s.into_iter().map(|s| s.to_string()).collect();
callback(&app_handle, args, cwd.to_string());
callback(app_handle, args, cwd.to_string());
}
1
}

@ -4,6 +4,8 @@ Interface with SQL databases through [sqlx](https://github.com/launchbadge/sqlx)
## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)
@ -42,7 +44,7 @@ First you need to register the core plugin with Tauri:
```rust
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_sql::Builder::default())
.plugin(tauri_plugin_sql::Builder::default().build())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

@ -10,7 +10,7 @@ use sqlx::{
migrate::{
MigrateDatabase, Migration as SqlxMigration, MigrationSource, MigrationType, Migrator,
},
Column, Pool, Row, TypeInfo,
Column, Pool, Row, TypeInfo, ValueRef,
};
use tauri::{
command,
@ -44,6 +44,8 @@ pub enum Error {
Migration(#[from] sqlx::migrate::MigrateError),
#[error("database {0} not loaded")]
DatabaseNotLoaded(String),
#[error("unsupported datatype: {0}")]
UnsupportedDatatype(String),
}
impl Serialize for Error {
@ -246,12 +248,16 @@ async fn select(
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() {
let v = row.try_get_raw(i)?;
let v = if v.is_null() {
JsonValue::Null
} else {
match info.name() {
"VARCHAR" | "STRING" | "TEXT" | "DATETIME" => {
// TODO: postgresql's JSON type
match v.type_info().name() {
"VARCHAR" | "STRING" | "TEXT" | "TINYTEXT" | "LONGTEXT" | "NVARCHAR"
| "BIGVARCHAR" | "CHAR" | "BIGCHAR" | "NCHAR" | "DATETIME" | "DATE"
| "TIME" | "YEAR" | "TIMESTAMP" => {
if let Ok(s) = row.try_get(i) {
JsonValue::String(s)
} else {
@ -266,22 +272,25 @@ async fn select(
JsonValue::Bool(x.to_lowercase() == "true")
}
}
"INT" | "NUMBER" | "INTEGER" | "BIGINT" | "INT8" => {
"INT" | "NUMBER" | "INTEGER" | "BIGINT" | "INT2" | "INT4" | "INT8"
| "NUMERIC" | "TINYINT" | "SMALLINT" | "MEDIUMINT" | "TINYINT UNSINGED"
| "SMALLINT UNSINGED" | "INT UNSINGED" | "MEDIUMINT UNSINGED"
| "BIGINT UNSINGED" => {
if let Ok(n) = row.try_get::<i64, usize>(i) {
JsonValue::Number(n.into())
} else {
JsonValue::Null
}
}
"REAL" => {
"REAL" | "FLOAT" | "DOUBLE" | "FLOAT4" | "FLOAT8" => {
if let Ok(n) = row.try_get::<f64, usize>(i) {
JsonValue::from(n)
} else {
JsonValue::Null
}
}
// "JSON" => JsonValue::Object(row.get(i)),
"BLOB" => {
"BLOB" | "TINYBLOB" | "MEDIUMBLOB" | "LONGBLOB" | "BINARY" | "VARBINARY"
| "BYTEA" => {
if let Ok(n) = row.try_get::<Vec<u8>, usize>(i) {
JsonValue::Array(
n.into_iter().map(|n| JsonValue::Number(n.into())).collect(),
@ -290,13 +299,16 @@ async fn select(
JsonValue::Null
}
}
_ => JsonValue::Null,
_ => return Err(Error::UnsupportedDatatype(v.type_info().name().to_string())),
}
};
value.insert(column.name().to_string(), v);
}
values.push(value);
}
Ok(values)
}

@ -4,6 +4,8 @@ Simple, persistent key-value store.
## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

@ -4,6 +4,8 @@ Store secrets and keys using the [IOTA Stronghold](https://github.com/iotaledger
## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

@ -18,5 +18,5 @@ thiserror.workspace = true
tokio = { version = "1", features = [ "fs" ] }
tokio-util = { version = "0.7", features = [ "codec" ] }
reqwest = { version = "0.11", features = [ "json", "stream" ] }
futures = "0.3"
futures-util = "0.3"
read-progress-stream = "1.0.0"

@ -4,6 +4,8 @@ Upload files from disk to a remote server over HTTP.
## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

@ -11,12 +11,12 @@ type ProgressHandler = (progress: number, total: number) => void;
const handlers: Map<number, ProgressHandler> = new Map();
let listening = false;
async function listenToUploadEventIfNeeded(): Promise<void> {
async function listenToEventIfNeeded(event: string): Promise<void> {
if (listening) {
return await Promise.resolve();
}
return await appWindow
.listen<ProgressPayload>("upload://progress", ({ payload }) => {
.listen<ProgressPayload>(event, ({ payload }) => {
const handler = handlers.get(payload.id);
if (handler != null) {
handler(payload.progress, payload.total);
@ -27,7 +27,7 @@ async function listenToUploadEventIfNeeded(): Promise<void> {
});
}
export default async function upload(
async function upload(
url: string,
filePath: string,
progressHandler?: ProgressHandler,
@ -41,7 +41,7 @@ export default async function upload(
handlers.set(id, progressHandler);
}
await listenToUploadEventIfNeeded();
await listenToEventIfNeeded("upload://progress");
await invoke("plugin:upload|upload", {
id,
@ -50,3 +50,30 @@ export default async function upload(
headers: headers ?? {},
});
}
async function download(
url: string,
filePath: string,
progressHandler?: ProgressHandler,
headers?: Map<string, string>
): Promise<void> {
const ids = new Uint32Array(1);
window.crypto.getRandomValues(ids);
const id = ids[0];
if (progressHandler != null) {
handlers.set(id, progressHandler);
}
await listenToEventIfNeeded("download://progress");
await invoke("plugin:upload|upload", {
id,
url,
filePath,
headers: headers ?? {},
});
}
export default upload;
export { download, upload };

@ -2,14 +2,14 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use futures::TryStreamExt;
use futures_util::TryStreamExt;
use serde::{ser::Serializer, Serialize};
use tauri::{
command,
plugin::{Builder as PluginBuilder, TauriPlugin},
Runtime, Window,
};
use tokio::fs::File;
use tokio::{fs::File, io::AsyncWriteExt};
use tokio_util::codec::{BytesCodec, FramedRead};
use read_progress_stream::ReadProgressStream;
@ -24,6 +24,8 @@ pub enum Error {
Io(#[from] std::io::Error),
#[error(transparent)]
Request(#[from] reqwest::Error),
#[error("{0}")]
ContentLength(String),
}
impl Serialize for Error {
@ -42,6 +44,46 @@ struct ProgressPayload {
total: u64,
}
#[command]
async fn download<R: Runtime>(
window: Window<R>,
id: u32,
url: &str,
file_path: &str,
headers: HashMap<String, String>,
) -> Result<u32> {
let client = reqwest::Client::new();
let mut request = client.get(url);
// Loop trought the headers keys and values
// and add them to the request object.
for (key, value) in headers {
request = request.header(&key, value);
}
let response = request.send().await?;
let total = response.content_length().ok_or_else(|| {
Error::ContentLength(format!("Failed to get content length from '{}'", url))
})?;
let mut file = File::create(file_path).await?;
let mut stream = response.bytes_stream();
while let Some(chunk) = stream.try_next().await? {
file.write_all(&chunk).await?;
let _ = window.emit(
"download://progress",
ProgressPayload {
id,
progress: chunk.len() as u64,
total,
},
);
}
Ok(id)
}
#[command]
async fn upload<R: Runtime>(
window: Window<R>,
@ -88,6 +130,6 @@ fn file_to_body<R: Runtime>(id: u32, window: Window<R>, file: File) -> reqwest::
pub fn init<R: Runtime>() -> TauriPlugin<R> {
PluginBuilder::new("upload")
.invoke_handler(tauri::generate_handler![upload])
.invoke_handler(tauri::generate_handler![download, upload])
.build()
}

@ -4,6 +4,8 @@
## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

@ -15,4 +15,5 @@ serde_json.workspace = true
tauri.workspace = true
log.workspace = true
thiserror.workspace = true
bincode = "1.3"
bincode = "1.3"
bitflags = "1"

@ -1,9 +1,11 @@
![plugin-window-state](banner.png)
Save window positions and sizse and restore them when the app is reopened.
Save window positions and sizes and restore them when the app is reopened.
## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)
@ -39,19 +41,19 @@ Afterwards all windows will remember their state when the app is being closed an
Optionally you can also tell the plugin to save the state of all open window to disk my using the `save_window_state()` method exposed by the `AppHandleExt` trait:
```rust
use tauri_plugin_window_state::AppHandleExt;
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
// `tauri::AppHandle` now has the following additional method
app.save_window_state(); // will save the state of all open windows to disk
app.save_window_state(StateFlags::all()); // will save the state of all open windows to disk
```
To manually restore a windows state from disk you can call the `restore_state()` method exposed by the `WindowExt` trait:
```rust
use tauri_plugin_window_state::{WindowExt, ShowMode};
use tauri_plugin_window_state::{WindowExt, StateFlags};
// all `Window` types now have the following additional method
window.restore_state(ShowMode::LastSaved); // will restore the windows state from disk
window.restore_state(StateFlags::all()); // will restore the windows state from disk
```
## Contributing

@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use bitflags::bitflags;
use serde::{Deserialize, Serialize};
use tauri::{
plugin::{Builder as PluginBuilder, TauriPlugin},
@ -30,27 +31,27 @@ pub enum Error {
Bincode(#[from] Box<bincode::ErrorKind>),
}
/// Defines how the window visibility should be restored.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum ShowMode {
/// The window will always be shown, regardless of what the last stored state was.
Always,
/// The window will be automatically shown if the last stored state for visibility was `true`.
LastSaved,
/// The window will not be automatically shown by this plugin.
Never,
pub type Result<T> = std::result::Result<T, Error>;
bitflags! {
pub struct StateFlags: u32 {
const SIZE = 1 << 0;
const POSITION = 1 << 1;
const MAXIMIZED = 1 << 2;
const VISIBLE = 1 << 3;
const DECORATIONS = 1 << 4;
const FULLSCREEN = 1 << 5;
}
}
impl Default for ShowMode {
impl Default for StateFlags {
fn default() -> Self {
Self::LastSaved
Self::all()
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Default, Deserialize, Serialize)]
struct WindowMetadata {
struct WindowState {
width: f64,
height: f64,
x: i32,
@ -61,17 +62,23 @@ struct WindowMetadata {
fullscreen: bool,
}
struct WindowStateCache(Arc<Mutex<HashMap<String, WindowMetadata>>>);
struct WindowStateCache(Arc<Mutex<HashMap<String, WindowState>>>);
pub trait AppHandleExt {
fn save_window_state(&self) -> Result<()>;
fn save_window_state(&self, flags: StateFlags) -> Result<()>;
}
impl<R: Runtime> AppHandleExt for tauri::AppHandle<R> {
fn save_window_state(&self) -> Result<()> {
fn save_window_state(&self, flags: StateFlags) -> Result<()> {
if let Some(app_dir) = self.path_resolver().app_config_dir() {
let state_path = app_dir.join(STATE_FILENAME);
let cache = self.state::<WindowStateCache>();
let state = cache.0.lock().unwrap();
let mut state = cache.0.lock().unwrap();
for (label, s) in state.iter_mut() {
if let Some(window) = self.get_window(label) {
window.update_state(s, flags)?;
}
}
create_dir_all(&app_dir)
.map_err(Error::Io)
.and_then(|_| File::create(state_path).map_err(Into::into))
@ -86,68 +93,146 @@ impl<R: Runtime> AppHandleExt for tauri::AppHandle<R> {
}
pub trait WindowExt {
fn restore_state(&self, show_mode: ShowMode) -> tauri::Result<()>;
fn restore_state(&self, flags: StateFlags) -> tauri::Result<()>;
}
impl<R: Runtime> WindowExt for Window<R> {
fn restore_state(&self, show_mode: ShowMode) -> tauri::Result<()> {
fn restore_state(&self, flags: StateFlags) -> tauri::Result<()> {
let cache = self.state::<WindowStateCache>();
let mut c = cache.0.lock().unwrap();
let mut should_show = true;
if let Some(state) = c.get(self.label()) {
self.set_decorations(state.decorated)?;
self.set_size(LogicalSize {
width: state.width,
height: state.height,
})?;
// restore position to saved value if saved monitor exists
// otherwise, let the OS decide where to place the window
for m in self.available_monitors()? {
if m.contains((state.x, state.y).into()) {
self.set_position(PhysicalPosition {
x: state.x,
y: state.y,
})?;
if flags.contains(StateFlags::DECORATIONS) {
self.set_decorations(state.decorated)?;
}
if flags.contains(StateFlags::SIZE) {
self.set_size(LogicalSize {
width: state.width,
height: state.height,
})?;
}
if flags.contains(StateFlags::POSITION) {
// restore position to saved value if saved monitor exists
// otherwise, let the OS decide where to place the window
for m in self.available_monitors()? {
if m.contains((state.x, state.y).into()) {
self.set_position(PhysicalPosition {
x: state.x,
y: state.y,
})?;
}
}
}
if state.maximized {
if flags.contains(StateFlags::MAXIMIZED) && state.maximized {
self.maximize()?;
}
self.set_fullscreen(state.fullscreen)?;
if flags.contains(StateFlags::FULLSCREEN) {
self.set_fullscreen(state.fullscreen)?;
}
should_show = state.visible;
} else {
let mut metadata = WindowState::default();
if flags.contains(StateFlags::SIZE) {
let scale_factor = self
.current_monitor()?
.map(|m| m.scale_factor())
.unwrap_or(1.);
let size = self.inner_size()?.to_logical(scale_factor);
metadata.width = size.width;
metadata.height = size.height;
}
if flags.contains(StateFlags::POSITION) {
let pos = self.outer_position()?;
metadata.x = pos.x;
metadata.y = pos.y;
}
if flags.contains(StateFlags::MAXIMIZED) {
metadata.maximized = self.is_maximized()?;
}
if flags.contains(StateFlags::VISIBLE) {
metadata.visible = self.is_visible()?;
}
if flags.contains(StateFlags::DECORATIONS) {
metadata.visible = self.is_visible()?;
}
if flags.contains(StateFlags::FULLSCREEN) {
metadata.fullscreen = self.is_fullscreen()?;
}
c.insert(self.label().into(), metadata);
}
if flags.contains(StateFlags::VISIBLE) && should_show {
self.show()?;
self.set_focus()?;
}
Ok(())
}
}
trait WindowExtInternal {
fn update_state(&self, state: &mut WindowState, flags: StateFlags) -> tauri::Result<()>;
}
impl<R: Runtime> WindowExtInternal for Window<R> {
fn update_state(&self, state: &mut WindowState, flags: StateFlags) -> tauri::Result<()> {
let is_maximized = match flags.intersects(StateFlags::MAXIMIZED | StateFlags::SIZE) {
true => self.is_maximized()?,
false => false,
};
if flags.contains(StateFlags::MAXIMIZED) {
state.maximized = is_maximized;
}
if flags.contains(StateFlags::FULLSCREEN) {
state.fullscreen = self.is_fullscreen()?;
}
if flags.contains(StateFlags::DECORATIONS) {
state.decorated = self.is_decorated()?;
}
if flags.contains(StateFlags::VISIBLE) {
state.visible = self.is_visible()?;
}
if flags.contains(StateFlags::SIZE) {
let scale_factor = self
.current_monitor()?
.map(|m| m.scale_factor())
.unwrap_or(1.);
let LogicalSize { width, height } = self.inner_size()?.to_logical(scale_factor);
let PhysicalPosition { x, y } = self.outer_position()?;
let maximized = self.is_maximized().unwrap_or(false);
let visible = self.is_visible().unwrap_or(true);
let decorated = self.is_decorated().unwrap_or(true);
let fullscreen = self.is_fullscreen().unwrap_or(false);
c.insert(
self.label().into(),
WindowMetadata {
width,
height,
x,
y,
maximized,
visible,
decorated,
fullscreen,
},
);
let size = self.inner_size()?.to_logical(scale_factor);
// It doesn't make sense to save a self with 0 height or width
if size.width > 0. && size.height > 0. && !is_maximized {
state.width = size.width;
state.height = size.height;
}
}
if show_mode == ShowMode::Always || (show_mode == ShowMode::LastSaved && should_show) {
self.show()?;
self.set_focus()?;
if flags.contains(StateFlags::POSITION) {
let position = self.inner_position()?;
if let Ok(Some(monitor)) = self.current_monitor() {
// save only window positions that are inside the current monitor
if monitor.contains(position) && !is_maximized {
state.x = position.x;
state.y = position.y;
}
}
}
Ok(())
@ -156,17 +241,15 @@ impl<R: Runtime> WindowExt for Window<R> {
#[derive(Default)]
pub struct Builder {
show_mode: ShowMode,
denylist: HashSet<String>,
skip_initial_state: HashSet<String>,
state_flags: StateFlags,
}
impl Builder {
/// Sets how the window visibility should be restored.
///
/// The default is [`ShowMode::LastSaved`]
pub fn with_show_mode(mut self, show_mode: ShowMode) -> Self {
self.show_mode = show_mode;
/// Sets the state flags to control what state gets restored and saved.
pub fn with_state_flags(mut self, flags: StateFlags) -> Self {
self.state_flags = flags;
self
}
@ -184,9 +267,10 @@ impl Builder {
}
pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
let flags = self.state_flags;
PluginBuilder::new("window-state")
.setup(|app| {
let cache: Arc<Mutex<HashMap<String, WindowMetadata>>> = if let Some(app_dir) =
let cache: Arc<Mutex<HashMap<String, WindowState>>> = if let Some(app_dir) =
app.path_resolver().app_config_dir()
{
let state_path = app_dir.join(STATE_FILENAME);
@ -212,67 +296,26 @@ impl Builder {
}
if !self.skip_initial_state.contains(window.label()) {
let _ = window.restore_state(self.show_mode);
let _ = window.restore_state(self.state_flags);
}
let cache = window.state::<WindowStateCache>();
let cache = cache.0.clone();
let label = window.label().to_string();
let window_clone = window.clone();
window.on_window_event(move |e| match e {
WindowEvent::Moved(position) => {
let mut c = cache.lock().unwrap();
if let Some(state) = c.get_mut(&label) {
let is_maximized = window_clone.is_maximized().unwrap_or(false);
state.maximized = is_maximized;
if let Some(monitor) = window_clone.current_monitor().unwrap() {
let monitor_position = monitor.position();
// save only window positions that are inside the current monitor
if position.x > monitor_position.x
&& position.y > monitor_position.y
&& !is_maximized
{
state.x = position.x;
state.y = position.y;
};
};
}
}
WindowEvent::Resized(size) => {
let scale_factor = window_clone
.current_monitor()
.ok()
.map(|m| m.map(|m| m.scale_factor()).unwrap_or(1.))
.unwrap_or(1.);
let size = size.to_logical(scale_factor);
let mut c = cache.lock().unwrap();
if let Some(state) = c.get_mut(&label) {
let is_maximized = window_clone.is_maximized().unwrap_or(false);
let is_fullscreen = window_clone.is_fullscreen().unwrap_or(false);
state.decorated = window_clone.is_decorated().unwrap_or(true);
state.maximized = is_maximized;
state.fullscreen = is_fullscreen;
// It doesn't make sense to save a window with 0 height or width
if size.width > 0. && size.height > 0. && !is_maximized {
state.width = size.width;
state.height = size.height;
}
}
}
WindowEvent::CloseRequested { .. } => {
let flags = self.state_flags;
window.on_window_event(move |e| {
if let WindowEvent::CloseRequested { .. } = e {
let mut c = cache.lock().unwrap();
if let Some(state) = c.get_mut(&label) {
state.visible = window_clone.is_visible().unwrap_or(true);
let _ = window_clone.update_state(state, flags);
}
}
_ => {}
});
})
.on_event(|app, event| {
.on_event(move |app, event| {
if let RunEvent::Exit = event {
let _ = app.save_window_state();
let _ = app.save_window_state(flags);
}
})
.build()

@ -5,13 +5,13 @@ importers:
.:
specifiers:
'@rollup/plugin-node-resolve': ^15.0.1
'@rollup/plugin-terser': ^0.3.0
'@rollup/plugin-terser': ^0.4.0
'@rollup/plugin-typescript': ^11.0.0
'@typescript-eslint/eslint-plugin': ^5.0.0
'@typescript-eslint/parser': ^5.46.1
eslint: ^8.0.1
eslint-config-prettier: ^8.5.0
eslint-config-standard-with-typescript: ^27.0.0
eslint-config-standard-with-typescript: ^34.0.0
eslint-plugin-import: ^2.25.2
eslint-plugin-n: ^15.0.0
eslint-plugin-promise: ^6.0.0
@ -20,13 +20,13 @@ importers:
typescript: ^4.9.4
devDependencies:
'@rollup/plugin-node-resolve': 15.0.1_rollup@3.7.4
'@rollup/plugin-terser': 0.3.0_rollup@3.7.4
'@rollup/plugin-terser': 0.4.0_rollup@3.7.4
'@rollup/plugin-typescript': 11.0.0_fhibmf72xnv5tve6nwed265eae
'@typescript-eslint/eslint-plugin': 5.46.1_imrg37k3svwu377c6q7gkarwmi
'@typescript-eslint/parser': 5.46.1_ha6vam6werchizxrnqvarmz2zu
eslint: 8.29.0
eslint-config-prettier: 8.5.0_eslint@8.29.0
eslint-config-standard-with-typescript: 27.0.1_f7skxvi3ubnnb5utlsc5vholvm
eslint-config-standard-with-typescript: 34.0.0_f7skxvi3ubnnb5utlsc5vholvm
eslint-plugin-import: 2.26.0_z7hwuz3w5sq2sbhy7d4iqrnsvq
eslint-plugin-n: 15.6.0_eslint@8.29.0
eslint-plugin-promise: 6.1.1_eslint@8.29.0
@ -479,8 +479,8 @@ packages:
rollup: 3.7.4
dev: true
/@rollup/plugin-terser/0.3.0_rollup@3.7.4:
resolution: {integrity: sha512-mYTkNW9KjOscS/3QWU5LfOKsR3/fAAVDaqcAe2TZ7ng6pN46f+C7FOZbITuIW/neA+PhcjoKl7yMyB3XcmA4gw==}
/@rollup/plugin-terser/0.4.0_rollup@3.7.4:
resolution: {integrity: sha512-Ipcf3LPNerey1q9ZMjiaWHlNPEHNU/B5/uh9zXLltfEQ1lVSLLeZSgAtTPWGyw8Ip1guOeq+mDtdOlEj/wNxQw==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^2.x || ^3.x
@ -1218,8 +1218,8 @@ packages:
eslint: 8.29.0
dev: true
/eslint-config-standard-with-typescript/27.0.1_f7skxvi3ubnnb5utlsc5vholvm:
resolution: {integrity: sha512-jJVyJldqqiCu3uSA/FP0x9jCDMG+Bbl73twTSnp0aysumJrz4iaVqLl+tGgmPrv0R829GVs117Nmn47M1DDDXA==}
/eslint-config-standard-with-typescript/34.0.0_f7skxvi3ubnnb5utlsc5vholvm:
resolution: {integrity: sha512-zhCsI4/A0rJ1ma8sf3RLXYc0gc7yPmdTWRVXMh9dtqeUx3yBQyALH0wosHhk1uQ9QyItynLdNOtcHKNw8G7lQw==}
peerDependencies:
'@typescript-eslint/eslint-plugin': ^5.0.0
eslint: ^8.0.1

@ -1,4 +1,12 @@
{
"extends": ["config:base"],
"enabledManagers": ["cargo", "npm"]
"enabledManagers": ["cargo", "npm"],
"packageRules": [
{
"description": "Disable node/pnpm version updates",
"matchPackageNames": ["node", "pnpm"],
"matchDepTypes": ["engines", "packageManager"],
"enabled": false
}
]
}

@ -4,6 +4,8 @@
## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

Loading…
Cancel
Save