1#![cfg(any(windows, target_os = "linux"))]
2
3use crate::task::InstallTaskError;
4use crate::task::{escape_arg, path_utf8};
5use std::fmt::Write as _;
6use std::{io, path::PathBuf};
7
8use super::SetupTaskError;
9
10pub enum CreateShortcut {}
12
13fn common_prepare_install(mut c: CreateShortcutConfig) -> CreateShortcutConfig {
16 if c.app_id.is_empty() {
17 c.app_id = zng_env::about().windows_aumid().to_string()
18 }
19 if c.working_dir.as_os_str().is_empty() {
20 c.working_dir = c.target_file.parent().unwrap_or_else(|| std::path::Path::new("")).to_path_buf()
21 }
22 if c.name.is_empty()
23 && let Some(name) = c.link_file.file_name()
24 && let Some(name) = name.to_str()
25 {
26 c.name = name.to_owned();
27 }
28 c
29}
30
31impl super::SetupTask for CreateShortcut {
32 type InstallConfig = CreateShortcutConfig;
33
34 type PrepareInstall = PrepareInstallData;
35
36 type Install = InstallData;
37
38 fn task_type_id() -> super::TaskTypeId {
39 "zng-setup/CreateShortcut".into()
40 }
41
42 #[cfg(windows)]
43 async fn prepare_install(args: super::PrepareInstallArgs<Self>) -> Result<Self::PrepareInstall, SetupTaskError> {
44 let c = common_prepare_install(args.config);
45
46 let working_dir = path_utf8(c.working_dir)?;
47 let icon = path_utf8(c.icon)?;
48
49 let mut args = String::new();
50 let mut sep = "";
51 for arg in &c.args {
52 write!(&mut args, "{sep}{}", escape_arg(arg)).unwrap();
53 sep = " ";
54 }
55
56 Ok(PrepareInstallData {
57 link_file: c.link_file.with_added_extension("lnk"),
58 target_file: c.target_file,
59 working_dir,
60 arguments: args,
61 app_id: c.app_id,
62 name: c.name,
63 icon,
64 })
65 }
66
67 #[cfg(target_os = "linux")]
68 async fn prepare_install(args: super::PrepareInstallArgs<Self>) -> Result<Self::PrepareInstall, SetupTaskError> {
69 let c = common_prepare_install(args.config);
70
71 let mut desktop = "[Desktop Entry]\nVersion=1.0\nType=Application\n".to_owned();
72
73 write!(&mut desktop, "Exec={}", escape_arg(&path_utf8(c.target_file)?)).unwrap();
74 for arg in c.args {
75 write!(&mut desktop, " {}", escape_arg(&arg)).unwrap();
76 }
77 writeln!(&mut desktop).unwrap();
78
79 if !c.working_dir.as_os_str().is_empty() {
80 writeln!(&mut desktop, "Path={}", path_utf8(c.working_dir)?).unwrap();
81 }
82
83 if !c.icon.as_os_str().is_empty() {
84 writeln!(&mut desktop, "Icon={}", path_utf8(c.icon)?).unwrap();
85 }
86
87 let name = c
88 .name
89 .replace("\\", r"\\")
90 .replace("\n", r"\n")
91 .replace("\t", r"\t")
92 .replace("\r", r"\r");
93 if name.is_empty() {
94 return Err(SetupTaskError::io(
95 c.link_file,
96 io::Error::new(io::ErrorKind::InvalidData, "missing name"),
97 ));
98 }
99 writeln!(&mut desktop, "Name={name}").unwrap();
100
101 Ok(PrepareInstallData {
102 link_file: c.link_file,
103 desktop,
104 })
105 }
106
107 #[cfg(windows)]
108 async fn install(args: super::InstallArgs<Self>) -> Result<Self::Install, InstallTaskError<Self::Install>> {
109 let data = InstallData {
110 link_file: args.data.link_file.clone(),
111 };
112
113 let (sx, rx) = zng_task::channel::rendezvous();
115 let r = std::thread::spawn(move || {
116 let r = windows_install(args.data);
117 sx.send_blocking(()).unwrap();
118 r
119 });
120 let _ = rx.recv().await;
121 let r = match r.join() {
122 Ok(r) => r,
123 Err(p) => std::panic::resume_unwind(p),
124 };
125 if let Err(e) = r {
126 return Err(InstallTaskError {
127 error: SetupTaskError::other(e),
128 clean_data: Some(data),
131 });
132 }
133 Ok(data)
134 }
135
136 #[cfg(target_os = "linux")]
137 async fn install(args: super::InstallArgs<Self>) -> Result<Self::Install, InstallTaskError<Self::Install>> {
138 fn write(link_file: PathBuf, desktop: String) -> io::Result<()> {
139 use std::io::Write as _;
140 use std::os::unix::fs::PermissionsExt as _;
141
142 let mut f = std::fs::File::create(&link_file)?;
143 f.write_all(desktop.as_bytes())?;
144
145 let mut perms = f.metadata()?.permissions();
146 perms.set_mode(0o755);
147 std::fs::set_permissions(link_file, perms)?;
148
149 Ok(())
150 }
151
152 let data = InstallData {
153 link_file: args.data.link_file.clone(),
154 };
155
156 let link_file = args.data.link_file.clone();
157
158 if let Err(e) = zng_task::wait(move || write(link_file, args.data.desktop)).await {
159 return Err(InstallTaskError {
160 error: SetupTaskError::io(args.data.link_file, e),
161 clean_data: Some(data),
164 });
165 }
166 Ok(data)
167 }
168
169 async fn cancel_install(_: super::CancelInstallArgs<Self>) -> Result<(), SetupTaskError> {
170 Ok(())
171 }
172
173 async fn validate_uninstall(args: super::ValidateUninstallArgs<Self>) -> Result<Self::Install, SetupTaskError> {
174 Ok(args.data)
175 }
176
177 async fn uninstall(args: super::UninstallArgs<Self>) -> Result<(), SetupTaskError> {
178 if let Err(e) = zng_task::fs::remove_file(&args.data.link_file).await
179 && !matches!(e.kind(), io::ErrorKind::NotFound)
180 {
181 return Err(SetupTaskError::io(args.data.link_file, e));
182 }
183 Ok(())
184 }
185}
186
187pub struct CreateShortcutConfig {
189 pub link_file: PathBuf,
192 pub target_file: PathBuf,
194 pub working_dir: PathBuf,
200 pub args: Vec<String>,
202
203 pub app_id: String,
217
218 pub name: String,
224 pub icon: PathBuf,
230}
231impl CreateShortcutConfig {
232 pub fn new(link_file: PathBuf, target_file: PathBuf) -> Self {
234 Self {
235 link_file,
236 target_file,
237 working_dir: PathBuf::new(),
238 args: vec![],
239 app_id: String::new(),
240 name: String::new(),
241 icon: PathBuf::new(),
242 }
243 }
244}
245
246#[cfg(target_os = "linux")]
247#[doc(hidden)]
248#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
249pub struct PrepareInstallData {
250 link_file: PathBuf,
251 desktop: String,
252}
253
254#[cfg(windows)]
255#[doc(hidden)]
256#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
257pub struct PrepareInstallData {
258 link_file: PathBuf,
259 target_file: PathBuf,
260 working_dir: String,
261 arguments: String,
262 app_id: String,
263 name: String,
264 icon: String,
265}
266
267#[doc(hidden)]
268#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
269pub struct InstallData {
270 link_file: PathBuf,
271}
272
273#[cfg(windows)]
275fn windows_install(d: PrepareInstallData) -> windows::core::Result<()> {
276 use std::path::Path;
277
278 use windows::{
279 Win32::{
280 Storage::EnhancedStorage::PKEY_AppUserModel_ID,
281 System::{
282 Com::{
283 CLSCTX_INPROC_SERVER, COINIT_MULTITHREADED, CoCreateInstance, CoInitializeEx, CoUninitialize, IPersistFile,
284 StructuredStorage::VariantToPropVariant,
285 },
286 Variant::VARIANT,
287 },
288 UI::Shell::{IShellLinkW, PropertiesSystem::IPropertyStore, ShellLink},
289 },
290 core::{Interface as _, PCWSTR},
291 };
292
293 fn wide(s: impl AsRef<std::ffi::OsStr>) -> Vec<u16> {
294 use std::os::windows::ffi::OsStrExt;
295
296 s.as_ref().encode_wide().chain(std::iter::once(0)).collect()
297 }
298
299 unsafe {
300 let _ok = CoInitializeEx(None, COINIT_MULTITHREADED).is_ok();
301 debug_assert!(_ok, "expected to run in a new thread");
302 struct ComDeinit;
303 impl Drop for ComDeinit {
304 fn drop(&mut self) {
305 unsafe {
306 CoUninitialize();
307 }
308 }
309 }
310 let _com_deinit = ComDeinit;
311
312 let link: IShellLinkW = CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER)?;
313
314 let target = wide(&d.target_file);
315 link.SetPath(PCWSTR(target.as_ptr()))?;
316
317 if !d.working_dir.is_empty() {
318 let value = wide(&d.working_dir);
319 link.SetWorkingDirectory(PCWSTR(value.as_ptr()))?;
320 }
321
322 if !d.arguments.is_empty() {
323 let value = wide(&d.arguments);
324 link.SetArguments(PCWSTR(value.as_ptr()))?;
325 }
326
327 if !d.name.is_empty() {
328 let value = wide(&d.name);
329 link.SetDescription(PCWSTR(value.as_ptr()))?;
330 }
331
332 if !d.icon.is_empty() {
333 let value = wide(&d.icon);
334 link.SetIconLocation(PCWSTR(value.as_ptr()), 0)?;
335 }
336
337 if !d.app_id.is_empty() {
339 let store: IPropertyStore = link.cast()?;
340
341 let variant = VARIANT::from(d.app_id.as_str());
342 let value = VariantToPropVariant(&variant)?;
343
344 store.SetValue(&PKEY_AppUserModel_ID, &value)?;
345 store.Commit()?;
346 }
347
348 let persist: IPersistFile = link.cast()?;
349 let output = wide(Path::new(&d.link_file));
350 persist.Save(PCWSTR(output.as_ptr()), true)?;
351 }
352 todo!()
353}