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