Skip to content

Commit a125c46

Browse files
committed
Switch windows implementation to use tauri-single-instance v2 methods
1 parent b357a6c commit a125c46

File tree

8 files changed

+301
-149
lines changed

8 files changed

+301
-149
lines changed

Cargo.toml

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
[package]
22
name = "cargo-deep-link"
33
version = "0.1.0"
4-
authors = ["FabianLars <[email protected]>", "jf908"]
4+
authors = [
5+
"jf908",
6+
"Tauri Programme within The Commons Conservancy",
7+
"FabianLars <[email protected]>",
8+
]
59
description = "A library for deep linking support"
610
repository = "https://github.com/jf908/cargo-deep-link"
711
edition = "2021"
@@ -15,13 +19,17 @@ dirs = "5"
1519
log = "0.4"
1620

1721
[target.'cfg(windows)'.dependencies]
18-
interprocess = { version = "1.2", default-features = false }
19-
windows-sys = { version = "0.52.0", features = [
22+
windows-sys = { version = "0.59.0", features = [
23+
"Win32_System_Threading",
24+
"Win32_System_DataExchange",
2025
"Win32_Foundation",
21-
"Win32_UI_Input_KeyboardAndMouse",
2226
"Win32_UI_WindowsAndMessaging",
27+
"Win32_Security",
28+
"Win32_System_LibraryLoader",
29+
"Win32_Graphics_Gdi",
2330
] }
24-
winreg = "0.55.0"
31+
dunce = "1"
32+
windows-registry = "0.4"
2533

2634
[target.'cfg(target_os = "macos")'.dependencies]
2735
objc2 = "0.4.1"

LICENSE_MIT

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
MIT License
22

3+
Copyright (c) 2025 - Present jf908
34
Copyright (c) 2022 - Present FabianLars
5+
Copyright (c) 2017 - Present Tauri Apps Contributors
46

57
Permission is hereby granted, free of charge, to any person obtaining a copy
68
of this software and associated documentation files (the "Software"), to deal

examples/main.rs

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
// Disable Windows console on release builds
2-
// #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
2+
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
33

4-
use eframe::egui;
4+
use std::sync::{
5+
Arc, Mutex,
6+
};
7+
8+
use eframe::egui::{self, ViewportCommand};
59

610
fn main() {
711
// prepare() checks if it's a single instance and tries to send the args otherwise.
@@ -10,30 +14,45 @@ fn main() {
1014

1115
env_logger::init();
1216

13-
cargo_deep_link::listen(|str| {
14-
println!("{:?}", str);
15-
})
16-
.unwrap();
17-
1817
eframe::run_native(
1918
"egui App",
2019
eframe::NativeOptions {
2120
viewport: egui::ViewportBuilder::default().with_inner_size([320.0, 240.0]),
2221
..Default::default()
2322
},
24-
Box::new(|_| Ok(Box::<App>::default())),
23+
Box::new(move |cc| {
24+
let frame = cc.egui_ctx.clone();
25+
let last_call = Arc::new(Mutex::new(None));
26+
27+
{
28+
let last_call = last_call.clone();
29+
30+
cargo_deep_link::listen(move |str| {
31+
last_call.lock().unwrap().replace(str);
32+
frame.send_viewport_cmd(ViewportCommand::Focus);
33+
})
34+
.unwrap();
35+
}
36+
37+
Ok(Box::new(App {
38+
last_call,
39+
..Default::default()
40+
}))
41+
}),
2542
)
2643
.unwrap();
2744
}
2845

2946
struct App {
3047
args: String,
48+
last_call: Arc<Mutex<Option<String>>>,
3149
}
3250

3351
impl Default for App {
3452
fn default() -> Self {
3553
Self {
3654
args: std::env::args().collect::<Vec<_>>().join(" "),
55+
last_call: Arc::new(Mutex::new(None)),
3756
}
3857
}
3958
}
@@ -43,23 +62,26 @@ impl eframe::App for App {
4362
egui::CentralPanel::default().show(ctx, |ui| {
4463
ui.heading("egui Application");
4564

46-
ui.label(self.args.clone());
65+
ui.horizontal(|ui| {
66+
if ui.add(egui::Button::new("Register")).clicked() {
67+
// If you need macOS support this must be called in .setup() !
68+
// Otherwise this could be called right after prepare() but then you don't have access to tauri APIs
69+
cargo_deep_link::register("test-scheme").unwrap(/* If listening to the scheme is optional for your app, you don't want to unwrap here. */);
70+
}
4771

48-
if ui.add(egui::Button::new("Register")).clicked() {
49-
// If you need macOS support this must be called in .setup() !
50-
// Otherwise this could be called right after prepare() but then you don't have access to tauri APIs
51-
cargo_deep_link::register(
52-
"test-scheme",
53-
|request| {
54-
println!("{:?}", &request);
55-
},
56-
)
57-
.unwrap(/* If listening to the scheme is optional for your app, you don't want to unwrap here. */);
58-
}
72+
if ui.add(egui::Button::new("Unregister")).clicked() {
73+
cargo_deep_link::unregister("test-scheme").unwrap();
74+
}
75+
});
5976

60-
if ui.add(egui::Button::new("Unregister")).clicked() {
61-
cargo_deep_link::unregister("test-scheme").unwrap();
62-
}
77+
ui.separator();
78+
79+
ui.label("Args:");
80+
81+
ui.text_edit_singleline(&mut self.args.clone());
82+
83+
ui.label("Last link:");
84+
ui.text_edit_singleline(&mut self.last_call.lock().unwrap().as_ref().cloned().unwrap_or_default());
6385
});
6486
}
6587
}

src/lib.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use std::{
44
sync::OnceLock,
55
};
66

7+
pub(crate) type SingleInstanceCallback = dyn FnMut(Vec<String>, String) + Send + Sync + 'static;
8+
79
#[cfg(target_os = "windows")]
810
#[path = "windows.rs"]
911
mod platform_impl;
@@ -16,6 +18,16 @@ mod platform_impl;
1618

1719
static ID: OnceLock<String> = OnceLock::new();
1820

21+
// pub fn init<F: FnMut(Vec<String>, String) + Send + Sync + 'static>(mut f: F) {
22+
// platform_impl::init(Box::new(move |args, cwd| {
23+
// #[cfg(feature = "deep-link")]
24+
// if let Some(deep_link) = app.try_state::<tauri_plugin_deep_link::DeepLink<R>>() {
25+
// deep_link.handle_cli_arguments(args.iter());
26+
// }
27+
// f(args, cwd)
28+
// }))
29+
// }
30+
1931
/// This function is meant for use-cases where the default [`prepare()`] function can't be used.
2032
///
2133
/// # Errors
@@ -32,16 +44,16 @@ pub fn set_identifier(identifier: &str) -> Result<()> {
3244
/// ## Platform-specific:
3345
///
3446
/// - **macOS**: On macOS schemes must be defined in an Info.plist file, therefore this function only calls [`listen()`] without registering the scheme. This function can only be called once on macOS.
35-
pub fn register<F: FnMut(String) + Send + 'static>(scheme: &str, handler: F) -> Result<()> {
36-
platform_impl::register(scheme, handler)
47+
pub fn register(scheme: &str) -> Result<()> {
48+
platform_impl::register(scheme)
3749
}
3850

3951
/// Starts the event listener without registering any schemes.
4052
///
4153
/// ## Platform-specific:
4254
///
4355
/// - **macOS**: This function can only be called once on macOS.
44-
pub fn listen<F: FnMut(String) + Send + 'static>(handler: F) -> Result<()> {
56+
pub fn listen<F: FnMut(String) + Send + Sync + 'static>(handler: F) -> Result<()> {
4557
platform_impl::listen(handler)
4658
}
4759

src/linux.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ use dirs::data_dir;
99

1010
use crate::ID;
1111

12-
pub fn register<F: FnMut(String) + Send + 'static>(scheme: &str, handler: F) -> Result<()> {
13-
listen(handler)?;
14-
12+
pub fn register(scheme: &str) -> Result<()> {
1513
let mut target = data_dir()
1614
.ok_or_else(|| Error::new(ErrorKind::NotFound, "data directory not found."))?
1715
.join("applications");

src/macos.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,7 @@ type THandler = OnceLock<Mutex<Box<dyn FnMut(String) + Send + 'static>>>;
2020
// If the Mutex turns out to be a problem, or FnMut turns out to be useless, we can remove the Mutex and turn FnMut into Fn
2121
static HANDLER: THandler = OnceLock::new();
2222

23-
pub fn register<F: FnMut(String) + Send + 'static>(_scheme: &str, handler: F) -> Result<()> {
24-
listen(handler)?;
25-
23+
pub fn register(_scheme: &str) -> Result<()> {
2624
Ok(())
2725
}
2826

src/windows.rs

Lines changed: 20 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -1,145 +1,53 @@
1-
use std::{
2-
io::{BufRead, BufReader, Result, Write},
3-
path::Path,
4-
};
1+
use std::io::Result;
52

6-
use interprocess::local_socket::{LocalSocketListener, LocalSocketStream};
7-
use windows_sys::Win32::UI::{
8-
Input::KeyboardAndMouse::{SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT},
9-
WindowsAndMessaging::{AllowSetForegroundWindow, ASFW_ANY},
10-
};
11-
use winreg::{enums::HKEY_CURRENT_USER, RegKey};
3+
use windows_registry::CURRENT_USER;
124

135
use crate::ID;
146

15-
pub fn register<F: FnMut(String) + Send + 'static>(scheme: &str, handler: F) -> Result<()> {
16-
listen(handler)?;
7+
mod windows_single;
178

18-
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
19-
let base = Path::new("Software").join("Classes").join(scheme);
9+
pub fn register(scheme: &str) -> Result<()> {
10+
let key_base = format!("Software\\Classes\\{}", scheme);
2011

21-
let exe = crate::current_exe()?
12+
let exe = dunce::simplified(&crate::current_exe()?)
2213
.display()
23-
.to_string()
24-
.replace("\\\\?\\", "");
14+
.to_string();
2515

26-
let (key, _) = hkcu.create_subkey(&base)?;
27-
key.set_value(
16+
let key_reg = CURRENT_USER.create(&key_base)?;
17+
key_reg.set_string(
2818
"",
2919
&format!(
30-
"URL:{}",
20+
"URL:{} protocol",
3121
ID.get().expect("register() called before prepare()")
3222
),
3323
)?;
34-
key.set_value("URL Protocol", &"")?;
24+
key_reg.set_string("URL Protocol", "")?;
3525

36-
let (icon, _) = hkcu.create_subkey(base.join("DefaultIcon"))?;
37-
icon.set_value("", &format!("{},0", &exe))?;
26+
let icon_reg = CURRENT_USER.create(format!("{key_base}\\DefaultIcon"))?;
27+
icon_reg.set_string("", &format!("{exe},0"))?;
3828

39-
let (cmd, _) = hkcu.create_subkey(base.join("shell").join("open").join("command"))?;
29+
let cmd_reg = CURRENT_USER.create(format!("{key_base}\\shell\\open\\command"))?;
4030

41-
cmd.set_value("", &format!("{} \"%1\"", &exe))?;
31+
cmd_reg.set_string("", &format!("\"{exe}\" \"%1\""))?;
4232

4333
Ok(())
4434
}
4535

4636
pub fn unregister(scheme: &str) -> Result<()> {
47-
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
48-
let base = Path::new("Software").join("Classes").join(scheme);
49-
50-
hkcu.delete_subkey_all(base)?;
37+
CURRENT_USER.remove_tree(format!("Software\\Classes\\{}", scheme))?;
5138

5239
Ok(())
5340
}
5441

55-
pub fn listen<F: FnMut(String) + Send + 'static>(mut handler: F) -> Result<()> {
56-
std::thread::spawn(move || {
57-
let listener =
58-
LocalSocketListener::bind(ID.get().expect("listen() called before prepare()").as_str())
59-
.expect("Can't create listener");
60-
61-
for conn in listener.incoming().filter_map(|c| {
62-
c.map_err(|error| log::error!("Incoming connection failed: {}", error))
63-
.ok()
64-
}) {
65-
// Listen for the launch arguments
66-
let mut conn = BufReader::new(conn);
67-
let mut buffer = String::new();
68-
if let Err(io_err) = conn.read_line(&mut buffer) {
69-
log::error!("Error reading incoming connection: {}", io_err.to_string());
70-
};
71-
buffer.pop();
72-
73-
handler(buffer);
74-
}
75-
});
42+
pub fn listen<F: FnMut(String) + Sync + Send + 'static>(mut handler: F) -> Result<()> {
43+
windows_single::init(Box::new(move |args, _| {
44+
handler(args.join(" "));
45+
}));
7646

7747
Ok(())
7848
}
7949

8050
pub fn prepare(identifier: &str) {
81-
if let Ok(mut conn) = LocalSocketStream::connect(identifier) {
82-
// We are the secondary instance.
83-
// Prep to activate primary instance by allowing another process to take focus.
84-
85-
// A workaround to allow AllowSetForegroundWindow to succeed - press a key.
86-
// This was originally used by Chromium: https://bugs.chromium.org/p/chromium/issues/detail?id=837796
87-
dummy_keypress();
88-
89-
let primary_instance_pid = conn.peer_pid().unwrap_or(ASFW_ANY);
90-
unsafe {
91-
let success = AllowSetForegroundWindow(primary_instance_pid) != 0;
92-
if !success {
93-
log::warn!("AllowSetForegroundWindow failed.");
94-
}
95-
}
96-
97-
if let Err(io_err) = conn.write_all(std::env::args().nth(1).unwrap_or_default().as_bytes())
98-
{
99-
log::error!(
100-
"Error sending message to primary instance: {}",
101-
io_err.to_string()
102-
);
103-
};
104-
let _ = conn.write_all(b"\n");
105-
std::process::exit(0);
106-
};
10751
ID.set(identifier.to_string())
10852
.expect("prepare() called more than once with different identifiers.");
10953
}
110-
111-
/// Send a dummy keypress event so AllowSetForegroundWindow can succeed
112-
fn dummy_keypress() {
113-
let keyboard_input_down = KEYBDINPUT {
114-
wVk: 0, // This doesn't correspond to any actual keyboard key, but should still function for the workaround.
115-
dwExtraInfo: 0,
116-
wScan: 0,
117-
time: 0,
118-
dwFlags: 0,
119-
};
120-
121-
let mut keyboard_input_up = keyboard_input_down;
122-
keyboard_input_up.dwFlags = 0x0002; // KEYUP flag
123-
124-
let input_down_u = INPUT_0 {
125-
ki: keyboard_input_down,
126-
};
127-
let input_up_u = INPUT_0 {
128-
ki: keyboard_input_up,
129-
};
130-
131-
let input_down = INPUT {
132-
r#type: INPUT_KEYBOARD,
133-
Anonymous: input_down_u,
134-
};
135-
136-
let input_up = INPUT {
137-
r#type: INPUT_KEYBOARD,
138-
Anonymous: input_up_u,
139-
};
140-
141-
let ipsize = std::mem::size_of::<INPUT>() as i32;
142-
unsafe {
143-
SendInput(2, [input_down, input_up].as_ptr(), ipsize);
144-
};
145-
}

0 commit comments

Comments
 (0)