millennium 1.0.0-beta.3

Create consistent, light, & secure apps that work on all platforms, using HTML, CSS, and JavaScript
Documentation
// Copyright 2022 pyke.io
//           2019-2021 Tauri Programme within The Commons Conservancy
//                     [https://tauri.studio/]
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::sync::Arc;

pub use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;

use crate::{
	hooks::{InvokeError, InvokeMessage, InvokeResolver},
	Config, Invoke, PackageInfo, Runtime, Window
};

mod app;
#[cfg(cli)]
mod cli;
#[cfg(clipboard_any)]
mod clipboard;
#[cfg(dialog_any)]
mod dialog;
mod event;
#[cfg(fs_any)]
mod file_system;
#[cfg(global_shortcut_any)]
mod global_shortcut;
#[cfg(http_any)]
mod http;
mod notification;
#[cfg(os_any)]
mod operating_system;
#[cfg(path_any)]
mod path;
#[cfg(process_any)]
mod process;
#[cfg(shell_any)]
mod shell;
mod window;

/// The context passed to the invoke handler.
pub struct InvokeContext<R: Runtime> {
	pub window: Window<R>,
	pub config: Arc<Config>,
	pub package_info: PackageInfo
}

#[cfg(test)]
impl<R: Runtime> Clone for InvokeContext<R> {
	fn clone(&self) -> Self {
		Self {
			window: self.window.clone(),
			config: self.config.clone(),
			package_info: self.package_info.clone()
		}
	}
}

/// The response for a JS `invoke` call.
pub struct InvokeResponse {
	json: Result<JsonValue>
}

impl<T: Serialize> From<T> for InvokeResponse {
	fn from(value: T) -> Self {
		Self {
			json: serde_json::to_value(value).map_err(Into::into)
		}
	}
}

#[derive(Deserialize)]
#[serde(tag = "module", content = "message")]
enum Module {
	App(app::Cmd),
	#[cfg(process_any)]
	Process(process::Cmd),
	#[cfg(fs_any)]
	Fs(file_system::Cmd),
	#[cfg(os_any)]
	Os(operating_system::Cmd),
	#[cfg(path_any)]
	Path(path::Cmd),
	Window(Box<window::Cmd>),
	#[cfg(shell_any)]
	Shell(shell::Cmd),
	Event(event::Cmd),
	#[cfg(dialog_any)]
	Dialog(dialog::Cmd),
	#[cfg(cli)]
	Cli(cli::Cmd),
	Notification(notification::Cmd),
	#[cfg(http_any)]
	Http(http::Cmd),
	#[cfg(global_shortcut_any)]
	GlobalShortcut(global_shortcut::Cmd),
	#[cfg(clipboard_any)]
	Clipboard(clipboard::Cmd)
}

impl Module {
	fn run<R: Runtime>(self, window: Window<R>, resolver: InvokeResolver<R>, config: Arc<Config>, package_info: PackageInfo) {
		let context = InvokeContext { window, config, package_info };
		match self {
			Self::App(cmd) => resolver.respond_async(async move { cmd.run(context).and_then(|r| r.json).map_err(InvokeError::from_anyhow) }),
			#[cfg(process_any)]
			Self::Process(cmd) => resolver.respond_async(async move { cmd.run(context).and_then(|r| r.json).map_err(InvokeError::from_anyhow) }),
			#[cfg(fs_any)]
			Self::Fs(cmd) => resolver.respond_async(async move { cmd.run(context).and_then(|r| r.json).map_err(InvokeError::from_anyhow) }),
			#[cfg(path_any)]
			Self::Path(cmd) => resolver.respond_async(async move { cmd.run(context).and_then(|r| r.json).map_err(InvokeError::from_anyhow) }),
			#[cfg(os_any)]
			Self::Os(cmd) => resolver.respond_async(async move { cmd.run(context).and_then(|r| r.json).map_err(InvokeError::from_anyhow) }),
			Self::Window(cmd) => resolver.respond_async(async move { cmd.run(context).await.and_then(|r| r.json).map_err(InvokeError::from_anyhow) }),
			#[cfg(shell_any)]
			Self::Shell(cmd) => resolver.respond_async(async move { cmd.run(context).and_then(|r| r.json).map_err(InvokeError::from_anyhow) }),
			Self::Event(cmd) => resolver.respond_async(async move { cmd.run(context).and_then(|r| r.json).map_err(InvokeError::from_anyhow) }),
			#[cfg(dialog_any)]
			Self::Dialog(cmd) => resolver.respond_async(async move { cmd.run(context).and_then(|r| r.json).map_err(InvokeError::from_anyhow) }),
			#[cfg(cli)]
			Self::Cli(cmd) => resolver.respond_async(async move { cmd.run(context).and_then(|r| r.json).map_err(InvokeError::from_anyhow) }),
			Self::Notification(cmd) => resolver.respond_async(async move { cmd.run(context).and_then(|r| r.json).map_err(InvokeError::from_anyhow) }),
			#[cfg(http_any)]
			Self::Http(cmd) => resolver.respond_async(async move { cmd.run(context).await.and_then(|r| r.json).map_err(InvokeError::from_anyhow) }),
			#[cfg(global_shortcut_any)]
			Self::GlobalShortcut(cmd) => resolver.respond_async(async move { cmd.run(context).and_then(|r| r.json).map_err(InvokeError::from_anyhow) }),
			#[cfg(clipboard_any)]
			Self::Clipboard(cmd) => resolver.respond_async(async move { cmd.run(context).and_then(|r| r.json).map_err(InvokeError::from_anyhow) })
		}
	}
}

pub(crate) fn handle<R: Runtime>(module: String, invoke: Invoke<R>, config: Arc<Config>, package_info: &PackageInfo) {
	let Invoke { message, resolver } = invoke;
	let InvokeMessage { mut payload, window, .. } = message;

	if let JsonValue::Object(ref mut obj) = payload {
		obj.insert("module".to_string(), JsonValue::String(module.clone()));
	}

	match serde_json::from_value::<Module>(payload) {
		Ok(module) => module.run(window, resolver, config, package_info.clone()),
		Err(e) => {
			let message = e.to_string();
			if message.starts_with("unknown variant") {
				let mut s = message.split('`');
				s.next();
				if let Some(unknown_variant_name) = s.next() {
					if unknown_variant_name == module {
						return resolver.reject(format!(
							"The `{}` module is not enabled in the allowlist. Enable the features you want to use in .millenniumrc and Cargo.toml.",
							module
						));
					} else if module == "Window" {
						return resolver.reject(window::into_allowlist_error(unknown_variant_name).to_string());
					}
				}
			}
			resolver.reject(message);
		}
	}
}