diff --git a/espanso/src/cli/service/mod.rs b/espanso/src/cli/service/mod.rs
index 4c8b567..27d0b93 100644
--- a/espanso/src/cli/service/mod.rs
+++ b/espanso/src/cli/service/mod.rs
@@ -30,6 +30,11 @@ mod unix;
#[cfg(not(target_os = "windows"))]
use unix::*;
+#[cfg(target_os = "windows")]
+mod win;
+#[cfg(target_os = "windows")]
+use win::*;
+
mod stop;
pub fn new() -> CliModule {
@@ -47,6 +52,7 @@ pub fn new() -> CliModule {
fn service_main(args: CliModuleArgs) -> i32 {
let paths = args.paths.expect("missing paths argument");
let cli_args = args.cli_args.expect("missing cli_args");
+ #[allow(unused_variables)]
let paths_overrides = args.paths_overrides.expect("missing paths_overrides");
if cli_args.subcommand_matches("register").is_some() {
diff --git a/espanso/src/cli/service/win.rs b/espanso/src/cli/service/win.rs
new file mode 100644
index 0000000..d2f7faa
--- /dev/null
+++ b/espanso/src/cli/service/win.rs
@@ -0,0 +1,161 @@
+/*
+ * This file is part of espanso.
+ *
+ * Copyright (C) 2019-2021 Federico Terzi
+ *
+ * espanso is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * espanso is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with espanso. If not, see .
+ */
+
+use anyhow::Result;
+use std::path::{Path, PathBuf};
+use std::process::Command;
+use std::{fs::create_dir_all};
+use thiserror::Error;
+use std::os::windows::process::CommandExt;
+
+use crate::{error_eprintln, warn_eprintln};
+
+pub fn register() -> Result<()> {
+ let current_path = std::env::current_exe().expect("unable to get exec path");
+
+ let shortcut_path = get_startup_shortcut_file()?;
+
+ create_shortcut_target_file(&shortcut_path, ¤t_path, "launcher")
+}
+
+pub fn unregister() -> Result<()> {
+ let shortcut_path = get_startup_shortcut_file()?;
+ if !shortcut_path.is_file() {
+ error_eprintln!("could not unregister espanso, as it's not registered");
+ return Err(UnregisterError::EntryNotFound.into());
+ }
+
+ std::fs::remove_file(shortcut_path)?;
+
+ Ok(())
+}
+
+#[derive(Error, Debug)]
+pub enum UnregisterError {
+ #[error("entry not found")]
+ EntryNotFound,
+}
+
+pub fn is_registered() -> bool {
+ match get_startup_shortcut_file() {
+ Ok(shortcut_path) => {
+ if !shortcut_path.is_file() {
+ return false;
+ }
+
+ match get_shortcut_target_file(&shortcut_path) {
+ Ok(target_path) => {
+ // Check if the target file is the same as the current binary
+ let current_path = std::env::current_exe().expect("unable to get exec path");
+
+ if current_path != target_path {
+ warn_eprintln!("WARNING: Espanso is already registered as a service, but it points to another executable,");
+ warn_eprintln!("which can create some inconsistencies.");
+ warn_eprintln!("To fix the problem, unregister and register espanso again with these commands:");
+ warn_eprintln!("");
+ warn_eprintln!(" espanso service unregister");
+ warn_eprintln!(" espanso service register");
+ warn_eprintln!("");
+ }
+
+ true
+ },
+ Err(err) => {
+ error_eprintln!("unable to determine shortcut target path: {}", err);
+ false
+ },
+ }
+ }
+ Err(err) => {
+ error_eprintln!("could not locate shortcut file: {}", err);
+ false
+ }
+ }
+}
+
+pub fn start_service() -> Result<()> {
+ let current_path = std::env::current_exe().expect("unable to get exec path");
+
+ Command::new(current_path)
+ .args(&["launcher"])
+ .creation_flags(0x08000008) // CREATE_NO_WINDOW + DETACHED_PROCESS
+ .spawn()?;
+
+ Ok(())
+}
+
+fn get_startup_dir() -> Result {
+ let home_dir = dirs::home_dir().expect("unable to obtain user's home folder");
+ let app_data = home_dir.join("AppData");
+ let roaming = app_data.join("Roaming");
+ let microsoft = roaming.join("Microsoft");
+ let windows = microsoft.join("Windows");
+ let start_menu = windows.join("Start Menu");
+ let programs = start_menu.join("Programs");
+ let startup = programs.join("Startup");
+
+ if !startup.is_dir() {
+ create_dir_all(&startup)?;
+ }
+
+ Ok(startup)
+}
+
+fn get_startup_shortcut_file() -> Result {
+ let parent = get_startup_dir()?;
+ Ok(parent.join("espanso.lnk"))
+}
+
+fn get_shortcut_target_file(shortcut_path: &Path) -> Result {
+ let output = Command::new("powershell")
+ .arg("-c")
+ .arg("$sh = New-Object -ComObject WScript.Shell; $target = $sh.CreateShortcut($env:TARGET_FILE_PATH).TargetPath; echo $target")
+ .env("TARGET_FILE_PATH", shortcut_path.to_string_lossy().to_string())
+ .output()?;
+
+ if !output.status.success() {
+ return Err(ShortcutError::PowershellNonZeroExitCode.into());
+ }
+
+ let raw_path = String::from_utf8_lossy(&output.stdout);
+ let path = PathBuf::from(raw_path.trim().to_string());
+ Ok(path)
+}
+
+fn create_shortcut_target_file(shortcut_path: &Path, target_path: &Path, arguments: &str) -> Result<()> {
+ let output = Command::new("powershell")
+ .arg("-c")
+ .arg("$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut($env:SHORTCUT_PATH); $Shortcut.TargetPath = $env:TARGET_PATH; $Shortcut.Arguments = $env:TARGET_ARGS; $Shortcut.Save()")
+ .env("SHORTCUT_PATH", shortcut_path.to_string_lossy().to_string())
+ .env("TARGET_PATH", target_path.to_string_lossy().to_string())
+ .env("TARGET_ARGS", arguments)
+ .output()?;
+
+ if !output.status.success() {
+ return Err(ShortcutError::PowershellNonZeroExitCode.into());
+ }
+
+ Ok(())
+}
+
+#[derive(Error, Debug)]
+pub enum ShortcutError {
+ #[error("powershell exit with non-zero code")]
+ PowershellNonZeroExitCode,
+}
diff --git a/espanso/src/logging/mod.rs b/espanso/src/logging/mod.rs
index 69ea0ff..826334a 100644
--- a/espanso/src/logging/mod.rs
+++ b/espanso/src/logging/mod.rs
@@ -121,6 +121,14 @@ macro_rules! info_println {
}
}
+#[macro_export]
+macro_rules! warn_eprintln {
+ ($($tts:tt)*) => {
+ eprintln!($($tts)*);
+ log::warn!($($tts)*);
+ }
+}
+
#[macro_export]
macro_rules! error_eprintln {
($($tts:tt)*) => {