use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use crate::error::{Error, Result};
const WINDOWS_ONLY: &str = "win-desktop-utils operation requires Windows";
fn unsupported<T>() -> Result<T> {
Err(Error::Unsupported(WINDOWS_ONLY))
}
#[cfg(feature = "instance")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum InstanceScope {
CurrentSession,
Global,
}
#[cfg(feature = "instance")]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SingleInstanceOptions {
app_id: String,
scope: InstanceScope,
}
#[cfg(feature = "instance")]
impl SingleInstanceOptions {
pub fn new(app_id: impl Into<String>) -> Self {
Self {
app_id: app_id.into(),
scope: InstanceScope::CurrentSession,
}
}
pub fn current_session(app_id: impl Into<String>) -> Self {
Self::new(app_id)
}
pub fn global(app_id: impl Into<String>) -> Self {
Self::new(app_id).scope(InstanceScope::Global)
}
pub fn scope(mut self, scope: InstanceScope) -> Self {
self.scope = scope;
self
}
pub fn app_id(&self) -> &str {
&self.app_id
}
pub fn configured_scope(&self) -> InstanceScope {
self.scope
}
pub fn acquire(&self) -> Result<Option<InstanceGuard>> {
single_instance_with_options(self)
}
}
#[cfg(feature = "instance")]
#[must_use = "keep this guard alive for as long as you want to hold the single-instance lock"]
#[derive(Debug)]
pub struct InstanceGuard {
_private: (),
}
#[cfg(feature = "instance")]
pub fn single_instance(app_id: &str) -> Result<Option<InstanceGuard>> {
let _ = app_id;
unsupported()
}
#[cfg(feature = "instance")]
pub fn single_instance_with_scope(
app_id: &str,
scope: InstanceScope,
) -> Result<Option<InstanceGuard>> {
let _ = (app_id, scope);
unsupported()
}
#[cfg(feature = "instance")]
pub fn single_instance_with_options(
options: &SingleInstanceOptions,
) -> Result<Option<InstanceGuard>> {
let _ = options;
unsupported()
}
#[cfg(feature = "paths")]
pub fn roaming_app_data(app_name: &str) -> Result<PathBuf> {
let _ = app_name;
unsupported()
}
#[cfg(feature = "paths")]
pub fn local_app_data(app_name: &str) -> Result<PathBuf> {
let _ = app_name;
unsupported()
}
#[cfg(feature = "paths")]
pub fn ensure_roaming_app_data(app_name: &str) -> Result<PathBuf> {
let _ = app_name;
unsupported()
}
#[cfg(feature = "paths")]
pub fn ensure_local_app_data(app_name: &str) -> Result<PathBuf> {
let _ = app_name;
unsupported()
}
#[cfg(feature = "app")]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DesktopApp {
company_name: Option<String>,
app_name: String,
app_dir_name: String,
app_id: String,
instance_scope: InstanceScope,
}
#[cfg(feature = "app")]
impl DesktopApp {
pub fn new(app_name: impl Into<String>) -> Result<Self> {
let app_name = validate_identity_part("app_name", app_name.into())?;
Ok(Self {
company_name: None,
app_dir_name: app_name.clone(),
app_id: app_name.clone(),
app_name,
instance_scope: InstanceScope::CurrentSession,
})
}
pub fn with_company(
company_name: impl Into<String>,
app_name: impl Into<String>,
) -> Result<Self> {
let company_name = validate_identity_part("company_name", company_name.into())?;
let app_name = validate_identity_part("app_name", app_name.into())?;
Ok(Self {
app_dir_name: format!("{company_name}\\{app_name}"),
app_id: format!("{company_name}.{app_name}"),
company_name: Some(company_name),
app_name,
instance_scope: InstanceScope::CurrentSession,
})
}
pub fn instance_scope(mut self, scope: InstanceScope) -> Self {
self.instance_scope = scope;
self
}
pub fn company_name(&self) -> Option<&str> {
self.company_name.as_deref()
}
pub fn app_name(&self) -> &str {
&self.app_name
}
pub fn app_dir_name(&self) -> &str {
&self.app_dir_name
}
pub fn app_id(&self) -> &str {
&self.app_id
}
pub fn configured_instance_scope(&self) -> InstanceScope {
self.instance_scope
}
pub fn local_data_dir(&self) -> Result<PathBuf> {
local_app_data(&self.app_dir_name)
}
pub fn roaming_data_dir(&self) -> Result<PathBuf> {
roaming_app_data(&self.app_dir_name)
}
pub fn ensure_local_data_dir(&self) -> Result<PathBuf> {
ensure_local_app_data(&self.app_dir_name)
}
pub fn ensure_roaming_data_dir(&self) -> Result<PathBuf> {
ensure_roaming_app_data(&self.app_dir_name)
}
pub fn single_instance_options(&self) -> SingleInstanceOptions {
SingleInstanceOptions::new(self.app_id.clone()).scope(self.instance_scope)
}
pub fn single_instance(&self) -> Result<Option<InstanceGuard>> {
single_instance_with_options(&self.single_instance_options())
}
}
#[cfg(feature = "app")]
fn validate_identity_part(label: &'static str, value: String) -> Result<String> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(Error::InvalidInput(match label {
"company_name" => "company_name cannot be empty",
_ => "app_name cannot be empty",
}));
}
if trimmed.contains('\0') {
return Err(Error::InvalidInput(match label {
"company_name" => "company_name cannot contain NUL bytes",
_ => "app_name cannot contain NUL bytes",
}));
}
if trimmed
.chars()
.any(|ch| matches!(ch, '<' | '>' | ':' | '"' | '/' | '\\' | '|' | '?' | '*'))
{
return Err(Error::InvalidInput(match label {
"company_name" => "company_name contains invalid Windows file-name characters",
_ => "app_name contains invalid Windows file-name characters",
}));
}
Ok(trimmed.to_owned())
}
#[cfg(feature = "shell")]
pub fn open_with_default(target: impl AsRef<Path>) -> Result<()> {
let _ = target.as_ref();
unsupported()
}
#[cfg(feature = "shell")]
pub fn open_with_verb(verb: &str, target: impl AsRef<Path>) -> Result<()> {
let _ = (verb, target.as_ref());
unsupported()
}
#[cfg(feature = "shell")]
pub fn show_properties(target: impl AsRef<Path>) -> Result<()> {
let _ = target.as_ref();
unsupported()
}
#[cfg(feature = "shell")]
pub fn print_with_default(target: impl AsRef<Path>) -> Result<()> {
let _ = target.as_ref();
unsupported()
}
#[cfg(feature = "shell")]
pub fn open_url(url: &str) -> Result<()> {
let _ = url;
unsupported()
}
#[cfg(feature = "shell")]
pub fn reveal_in_explorer(path: impl AsRef<Path>) -> Result<()> {
let _ = path.as_ref();
unsupported()
}
#[cfg(feature = "shell")]
pub fn open_containing_folder(path: impl AsRef<Path>) -> Result<()> {
let _ = path.as_ref();
unsupported()
}
#[cfg(feature = "recycle-bin")]
pub fn move_to_recycle_bin(path: impl AsRef<Path>) -> Result<()> {
let _ = path.as_ref();
unsupported()
}
#[cfg(feature = "recycle-bin")]
pub fn move_paths_to_recycle_bin<I, P>(paths: I) -> Result<()>
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
{
let _ = paths.into_iter();
unsupported()
}
#[cfg(feature = "recycle-bin")]
pub fn empty_recycle_bin() -> Result<()> {
unsupported()
}
#[cfg(feature = "recycle-bin")]
pub fn empty_recycle_bin_for_root(root_path: impl AsRef<Path>) -> Result<()> {
let _ = root_path.as_ref();
unsupported()
}
#[cfg(feature = "shortcuts")]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ShortcutIcon {
pub path: PathBuf,
pub index: i32,
}
#[cfg(feature = "shortcuts")]
impl ShortcutIcon {
pub fn new(path: impl Into<PathBuf>, index: i32) -> Self {
Self {
path: path.into(),
index,
}
}
}
#[cfg(feature = "shortcuts")]
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ShortcutOptions {
pub arguments: Vec<OsString>,
pub working_directory: Option<PathBuf>,
pub icon: Option<ShortcutIcon>,
pub description: Option<String>,
}
#[cfg(feature = "shortcuts")]
impl ShortcutOptions {
pub fn new() -> Self {
Self::default()
}
pub fn arguments<I, S>(mut self, arguments: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<OsString>,
{
self.arguments = arguments.into_iter().map(Into::into).collect();
self
}
pub fn argument(mut self, argument: impl Into<OsString>) -> Self {
self.arguments.push(argument.into());
self
}
pub fn working_directory(mut self, path: impl Into<PathBuf>) -> Self {
self.working_directory = Some(path.into());
self
}
pub fn icon(mut self, path: impl Into<PathBuf>, index: i32) -> Self {
self.icon = Some(ShortcutIcon::new(path, index));
self
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
}
#[cfg(feature = "shortcuts")]
pub fn create_shortcut(
shortcut_path: impl AsRef<Path>,
target_path: impl AsRef<Path>,
options: &ShortcutOptions,
) -> Result<()> {
let _ = (shortcut_path.as_ref(), target_path.as_ref(), options);
unsupported()
}
#[cfg(feature = "shortcuts")]
pub fn create_url_shortcut(shortcut_path: impl AsRef<Path>, url: &str) -> Result<()> {
let _ = (shortcut_path.as_ref(), url);
unsupported()
}
#[cfg(feature = "elevation")]
pub fn is_elevated() -> Result<bool> {
unsupported()
}
#[cfg(feature = "elevation")]
pub fn restart_as_admin(args: &[OsString]) -> Result<()> {
let _ = args;
unsupported()
}
#[cfg(feature = "elevation")]
pub fn run_as_admin(executable: impl AsRef<OsStr>, args: &[OsString]) -> Result<()> {
let _ = (executable.as_ref(), args);
unsupported()
}
#[cfg(feature = "elevation")]
pub fn run_with_verb(verb: &str, executable: impl AsRef<OsStr>, args: &[OsString]) -> Result<()> {
let _ = (verb, executable.as_ref(), args);
unsupported()
}
#[cfg(test)]
mod tests {
#[cfg(feature = "paths")]
use super::local_app_data;
#[cfg(feature = "app")]
use super::DesktopApp;
#[cfg(feature = "shortcuts")]
use super::ShortcutOptions;
#[cfg(feature = "app")]
#[test]
fn desktop_app_keeps_identity_available_on_non_windows() {
let app = DesktopApp::with_company("Demo Company", "Demo App").unwrap();
assert_eq!(app.company_name(), Some("Demo Company"));
assert_eq!(app.app_dir_name(), "Demo Company\\Demo App");
assert_eq!(app.app_id(), "Demo Company.Demo App");
}
#[cfg(feature = "paths")]
#[test]
fn path_helpers_return_unsupported_on_non_windows() {
let result = local_app_data("Demo App");
assert!(matches!(result, Err(crate::Error::Unsupported(_))));
}
#[cfg(feature = "shortcuts")]
#[test]
fn shortcut_options_builder_sets_values_on_non_windows() {
let options = ShortcutOptions::new()
.argument("--demo")
.working_directory("/tmp")
.description("Demo");
assert_eq!(options.arguments.len(), 1);
assert_eq!(options.description.as_deref(), Some("Demo"));
assert!(options.working_directory.is_some());
}
}