From f16646f54091612eb92caf035450fe7d58ca629e Mon Sep 17 00:00:00 2001 From: Philip Kristoffersen Date: Fri, 27 May 2022 20:58:22 +0200 Subject: [PATCH] Add feature to disconnect a shortcut (#160) --- src/amazon/amazon_game.rs | 22 ++++- src/amazon/amazon_platform.rs | 12 ++- src/amazon/amazon_settings.rs | 2 +- src/egs/manifest_item.rs | 2 +- src/egs/paths.rs | 149 +++++++++++++++++----------------- src/gog/gog_platform.rs | 6 +- src/heroic/heroic_game.rs | 5 +- src/origin/origin_game.rs | 4 +- src/steam/utils.rs | 19 +++-- src/sync/mod.rs | 1 + src/sync/synchronization.rs | 21 ++++- src/ui/disconnect.rs | 1 + src/ui/mod.rs | 2 + src/ui/ui_disconnect.rs | 89 ++++++++++++++++++++ src/ui/ui_image_download.rs | 23 ++---- src/ui/ui_import_games.rs | 5 +- src/ui/ui_settings.rs | 5 +- src/ui/uiapp.rs | 13 ++- src/uplay/game.rs | 12 ++- 19 files changed, 271 insertions(+), 122 deletions(-) create mode 100644 src/ui/disconnect.rs create mode 100644 src/ui/ui_disconnect.rs diff --git a/src/amazon/amazon_game.rs b/src/amazon/amazon_game.rs index 1014886..b56f6f6 100644 --- a/src/amazon/amazon_game.rs +++ b/src/amazon/amazon_game.rs @@ -1,4 +1,4 @@ -use std::path::{PathBuf, Path}; +use std::path::{Path, PathBuf}; use steam_shortcuts_util::{shortcut::ShortcutOwned, Shortcut}; @@ -6,14 +6,28 @@ use steam_shortcuts_util::{shortcut::ShortcutOwned, Shortcut}; pub struct AmazonGame { pub title: String, pub id: String, - pub launcher_path:PathBuf, + pub launcher_path: PathBuf, } impl From for ShortcutOwned { fn from(game: AmazonGame) -> Self { let launch = format!("amazon-games://play/{}", game.id); let exe = game.launcher_path.to_string_lossy().to_string(); - let start_dir= game.launcher_path.parent().unwrap_or_else(||Path::new("")).to_string_lossy().to_string(); - Shortcut::new("0", game.title.as_str(), exe.as_str(), start_dir.as_str(), "", "", launch.as_str()).to_owned() + let start_dir = game + .launcher_path + .parent() + .unwrap_or_else(|| Path::new("")) + .to_string_lossy() + .to_string(); + Shortcut::new( + "0", + game.title.as_str(), + exe.as_str(), + start_dir.as_str(), + "", + "", + launch.as_str(), + ) + .to_owned() } } diff --git a/src/amazon/amazon_platform.rs b/src/amazon/amazon_platform.rs index 90e14b3..a59bfbf 100644 --- a/src/amazon/amazon_platform.rs +++ b/src/amazon/amazon_platform.rs @@ -31,7 +31,7 @@ impl Platform> for AmazonPlatform { fn get_shortcuts(&self) -> Result, Box> { let sqllite_path = get_sqlite_path().expect("This should never get called if settings are invalid"); - let launcher_path = + let launcher_path = get_launcher_path().expect("This should never get called if settings are invalid"); let mut result = vec![]; let connection = sqlite::open(sqllite_path)?; @@ -41,7 +41,11 @@ impl Platform> for AmazonPlatform { let id = statement.read::(0); let title = statement.read::(1); if let (Ok(id), Ok(title)) = (id, title) { - result.push(AmazonGame { title, id , launcher_path:launcher_path.clone()}); + result.push(AmazonGame { + title, + id, + launcher_path: launcher_path.clone(), + }); } } Ok(result) @@ -50,7 +54,7 @@ impl Platform> for AmazonPlatform { fn settings_valid(&self) -> crate::platform::SettingsValidity { let path = get_sqlite_path(); let launcher = get_launcher_path(); - if path.is_some() && launcher.is_some(){ + if path.is_some() && launcher.is_some() { crate::platform::SettingsValidity::Valid } else { crate::platform::SettingsValidity::Invalid { @@ -103,4 +107,4 @@ fn get_launcher_path() -> Option { } Err(_e) => None, } -} \ No newline at end of file +} diff --git a/src/amazon/amazon_settings.rs b/src/amazon/amazon_settings.rs index fb1672e..13cd240 100644 --- a/src/amazon/amazon_settings.rs +++ b/src/amazon/amazon_settings.rs @@ -3,5 +3,5 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize, Clone)] pub struct AmazonSettings { pub enabled: bool, - pub launcher_location: Option + pub launcher_location: Option, } diff --git a/src/egs/manifest_item.rs b/src/egs/manifest_item.rs index c479491..01b85aa 100644 --- a/src/egs/manifest_item.rs +++ b/src/egs/manifest_item.rs @@ -94,7 +94,7 @@ fn launcher_shortcut(manifest: ManifestItem) -> ShortcutOwned { .as_ref() .map(|p| { p.parent() - .unwrap_or_else(||Path::new("")) + .unwrap_or_else(|| Path::new("")) .to_string_lossy() .to_string() }) diff --git a/src/egs/paths.rs b/src/egs/paths.rs index b7702c5..21a30b0 100644 --- a/src/egs/paths.rs +++ b/src/egs/paths.rs @@ -81,95 +81,98 @@ mod unix { } } - #[cfg(target_os = "windows")] - mod windows { - use super::EpicPaths; - use std::{path::{Path, PathBuf}, env}; +#[cfg(target_os = "windows")] +mod windows { + use super::EpicPaths; + use std::{ + env, + path::{Path, PathBuf}, + }; - fn manifest_location_from_registry() -> Option { - use winreg::enums::*; - use winreg::RegKey; + fn manifest_location_from_registry() -> Option { + use winreg::enums::*; + use winreg::RegKey; - let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); - if let Ok(launcher) = - hklm.open_subkey("SOFTWARE\\WOW6432Node\\Epic Games\\EpicGamesLauncher") - { - let path_string: Result = launcher.get_value("AppDataPath"); - if let Ok(path_string) = path_string { - let path = Path::new(&path_string).join("Manifests"); - if path.exists() { - return Some(path); - } + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + if let Ok(launcher) = + hklm.open_subkey("SOFTWARE\\WOW6432Node\\Epic Games\\EpicGamesLauncher") + { + let path_string: Result = launcher.get_value("AppDataPath"); + if let Ok(path_string) = path_string { + let path = Path::new(&path_string).join("Manifests"); + if path.exists() { + return Some(path); } } - None } + None + } - fn guess_default_launcher_location() -> PathBuf { - let key = "SYSTEMDRIVE"; - let system_drive = - env::var(key).expect("We are on windows, we must know what the SYSTEMDRIVE is"); + fn guess_default_launcher_location() -> PathBuf { + let key = "SYSTEMDRIVE"; + let system_drive = + env::var(key).expect("We are on windows, we must know what the SYSTEMDRIVE is"); - let path = Path::new(format!("{}\\", system_drive).as_str()) - .join("Program Files (x86)") - .join("Epic Games") - .join("Launcher") - .join("Portal") - .join("Binaries") - .join("Win64") - .join("EpicGamesLauncher.exe"); - path - } + let path = Path::new(format!("{}\\", system_drive).as_str()) + .join("Program Files (x86)") + .join("Epic Games") + .join("Launcher") + .join("Portal") + .join("Binaries") + .join("Win64") + .join("EpicGamesLauncher.exe"); + path + } - fn launcher_location_from_registry() -> Option { - use winreg::enums::*; - use winreg::RegKey; + fn launcher_location_from_registry() -> Option { + use winreg::enums::*; + use winreg::RegKey; - let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); - if let Ok(launcher) = - hklm.open_subkey("SOFTWARE\\Classes\\com.epicgames.launcher\\shell\\open\\command") - { - let launch_string: Result = launcher.get_value(""); - if let Ok(launch_string) = launch_string { - let path = Path::new(&launch_string[1..launch_string.len() - 4]); - if path.exists() { - return Some(path.to_path_buf()); - } + if let Ok(launcher) = + hklm.open_subkey("SOFTWARE\\Classes\\com.epicgames.launcher\\shell\\open\\command") + { + let launch_string: Result = launcher.get_value(""); + if let Ok(launch_string) = launch_string { + let path = Path::new(&launch_string[1..launch_string.len() - 4]); + if path.exists() { + return Some(path.to_path_buf()); } } - None } + None + } - fn guess_default_manifest_location() -> PathBuf { - let key = "SYSTEMDRIVE"; - let system_drive = - env::var(key).expect("We are on windows, we must know what the SYSTEMDRIVE is"); + fn guess_default_manifest_location() -> PathBuf { + let key = "SYSTEMDRIVE"; + let system_drive = + env::var(key).expect("We are on windows, we must know what the SYSTEMDRIVE is"); - let path = Path::new(format!("{}\\", system_drive).as_str()) - .join("ProgramData") - .join("Epic") - .join("EpicGamesLauncher") - .join("Data") - .join("Manifests"); - path - } + let path = Path::new(format!("{}\\", system_drive).as_str()) + .join("ProgramData") + .join("Epic") + .join("EpicGamesLauncher") + .join("Data") + .join("Manifests"); + path + } - pub fn get_locations() -> Option { - { - let manifest_folder_path = manifest_location_from_registry() - .unwrap_or_else(guess_default_manifest_location); - let launcer_path = launcher_location_from_registry() - .unwrap_or_else(guess_default_launcher_location); - if launcer_path.exists() && manifest_folder_path.exists() { - Some(EpicPaths { - compat_folder_path: None, - manifest_folder_path, - launcher_path: launcer_path, - }) - } else { - None - } + pub fn get_locations() -> Option { + { + let manifest_folder_path = + manifest_location_from_registry().unwrap_or_else(guess_default_manifest_location); + let launcer_path = + launcher_location_from_registry().unwrap_or_else(guess_default_launcher_location); + if launcer_path.exists() && manifest_folder_path.exists() { + Some(EpicPaths { + compat_folder_path: None, + manifest_folder_path, + launcher_path: launcer_path, + }) + } else { + None } } } +} diff --git a/src/gog/gog_platform.rs b/src/gog/gog_platform.rs index 0c58b6f..0432040 100644 --- a/src/gog/gog_platform.rs +++ b/src/gog/gog_platform.rs @@ -57,10 +57,8 @@ fn get_shortcuts_from_games(games: Vec<(GogGame, PathBuf)>) -> Vec if let Some(primary_task) = tasks.iter().find(|t| { t.is_primary.unwrap_or_default() && t.task_type == "FileTask" - && ( - t.category.as_ref().unwrap_or(&String::from("")) == "launcher" || - t.category.as_ref().unwrap_or(&String::from("")) == "game" - ) + && (t.category.as_ref().unwrap_or(&String::from("")) == "launcher" + || t.category.as_ref().unwrap_or(&String::from("")) == "game") }) { if let Some(task_path) = &primary_task.path { let full_path = game_folder.join(&task_path); diff --git a/src/heroic/heroic_game.rs b/src/heroic/heroic_game.rs index ae5e4f1..5a2357b 100644 --- a/src/heroic/heroic_game.rs +++ b/src/heroic/heroic_game.rs @@ -33,7 +33,10 @@ impl From for ShortcutOwned { let (exe, parameter) = match game.install_mode.unwrap() { InstallationMode::FlatPak => ( "flatpak", - format!("run com.heroicgameslauncher.hgl {} --no-gui", launch_parameter), + format!( + "run com.heroicgameslauncher.hgl {} --no-gui", + launch_parameter + ), ), InstallationMode::UserBin => ("heroic", launch_parameter), }; diff --git a/src/origin/origin_game.rs b/src/origin/origin_game.rs index 0fdc21e..37c2c64 100644 --- a/src/origin/origin_game.rs +++ b/src/origin/origin_game.rs @@ -19,8 +19,8 @@ impl From for ShortcutOwned { None => format!( "\"origin2://game/launch?offerIds={}&autoDownload=1&authCode=&cmdParams=\"", game.id) - }; - let origin_location = format!("\"{}\"",game.origin_location.to_string_lossy()); + }; + let origin_location = format!("\"{}\"", game.origin_location.to_string_lossy()); let mut owned_shortcut = Shortcut::new( "0", game.title.as_str(), diff --git a/src/steam/utils.rs b/src/steam/utils.rs index 0fd70d7..3ca667a 100644 --- a/src/steam/utils.rs +++ b/src/steam/utils.rs @@ -128,14 +128,19 @@ pub fn get_default_location() -> Result> { #[cfg(target_os = "linux")] let path_string = { let home = std::env::var("HOME")?; - let default_path = Path::new(&home) + let default_path = Path::new(&home).join(".steam").join("steam"); + if default_path.exists() { + default_path.to_string_lossy().to_string() + } else { + Path::new(&home) + .join(".var") + .join("app") + .join("com.valvesoftware.Steam") .join(".steam") - .join("steam"); - if default_path.exists(){ - default_path.to_string_lossy().to_string() - }else{ - Path::new(&home).join(".var").join("app").join("com.valvesoftware.Steam").join(".steam").join("steam").to_string_lossy().to_string() - } + .join("steam") + .to_string_lossy() + .to_string() + } }; #[cfg(target_os = "macos")] let path_string = { diff --git a/src/sync/mod.rs b/src/sync/mod.rs index 8f977b0..4b4ca83 100644 --- a/src/sync/mod.rs +++ b/src/sync/mod.rs @@ -8,3 +8,4 @@ pub use synchronization::run_sync; pub use synchronization::IsBoilRShortcut; pub use synchronization::SyncProgress; +pub use synchronization::*; diff --git a/src/sync/synchronization.rs b/src/sync/synchronization.rs index 7da0f09..b0a7821 100644 --- a/src/sync/synchronization.rs +++ b/src/sync/synchronization.rs @@ -23,7 +23,7 @@ use std::error::Error; use crate::{gog::GogPlatform, itch::ItchPlatform, origin::OriginPlatform}; use std::{fs::File, io::Write, path::Path}; -const BOILR_TAG: &str = "boilr"; +pub const BOILR_TAG: &str = "boilr"; pub enum SyncProgress { NotStarted, @@ -34,6 +34,25 @@ pub enum SyncProgress { Done, } +pub fn disconnect_shortcut(settings: &Settings, app_id: u32) -> Result<(), String> { + let mut userinfo_shortcuts = get_shortcuts_paths(&settings.steam) + .map_err(|e| format!("Getting shortcut paths failed: {e}"))?; + + for user in userinfo_shortcuts.iter_mut() { + let mut shortcut_info = get_shortcuts_for_user(user); + + for shortcut in shortcut_info.shortcuts.iter_mut() { + if shortcut.app_id == app_id { + shortcut.dev_kit_game_id = "".to_string(); + shortcut.tags.retain(|s| s != BOILR_TAG); + } + } + save_shortcuts(&shortcut_info.shortcuts, Path::new(&shortcut_info.path)); + } + + Ok(()) +} + pub fn run_sync( settings: &Settings, sender: &mut Option>, diff --git a/src/ui/disconnect.rs b/src/ui/disconnect.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/ui/disconnect.rs @@ -0,0 +1 @@ + diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 051b7de..67e17f6 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,5 +1,6 @@ mod defines; mod ui_backup; +mod ui_disconnect; mod ui_image_download; mod ui_import_games; mod ui_settings; @@ -7,6 +8,7 @@ mod uiapp; pub use defines::*; pub use ui_backup::*; +pub use ui_disconnect::*; pub use ui_image_download::*; pub use ui_import_games::*; pub use ui_settings::*; diff --git a/src/ui/ui_disconnect.rs b/src/ui/ui_disconnect.rs new file mode 100644 index 0000000..76997ef --- /dev/null +++ b/src/ui/ui_disconnect.rs @@ -0,0 +1,89 @@ +use egui::ScrollArea; + +use super::ui_colors::*; +use super::MyEguiApp; +use crate::steam::get_shortcuts_for_user; +use crate::steam::get_shortcuts_paths; +use crate::steam::ShortcutInfo; +use crate::sync::disconnect_shortcut; +use crate::sync::IsBoilRShortcut; + +#[derive(Default)] +pub struct DiconnectState { + pub connected_shortcuts: Option, String>>, +} + +impl MyEguiApp { + pub fn render_disconnect(&mut self, ui: &mut egui::Ui) { + let steam_settings = self.settings.steam.clone(); + let users_info = self + .disconect_state + .connected_shortcuts + .get_or_insert_with(|| { + let users = get_shortcuts_paths(&steam_settings) + .map_err(|e| format!("Getting shortcut paths failed: {e}")); + users.map(|users| { + let mut user_info = vec![]; + for user in users { + let shortcut_info = get_shortcuts_for_user(&user); + user_info.push(shortcut_info); + } + user_info + }) + }); + + ui.heading("Add a disconnected Shortcuts"); + ui.label("In this section you can add a shortcut that BoilR is not in control of."); + ui.label("This prevents BoilR from deleting or updating a shortcut it orignally added."); + ui.label( + "This is useful if you want to manully edit a shortcut after BoilR has imported it.", + ); + + ui.add_space(super::SECTION_SPACING); + + match users_info.as_mut() { + Ok(users) => { + let has_multiple_users = users.len() > 1; + let mut redraw = 0; + set_scroll_style(ui); + ScrollArea::vertical() + .stick_to_right() + .auto_shrink([false, true]) + .show(ui, |ui| { + ui.reset_style(); + + for user in users.iter_mut() { + if has_multiple_users { + ui.heading(&user.path.to_string_lossy().to_string()); + } + for shortcut in user.shortcuts.iter() { + if shortcut.is_boilr_shortcut() + && ui.button(&shortcut.app_name).clicked() + { + if disconnect_shortcut(&self.settings, shortcut.app_id).is_ok() + { + redraw = shortcut.app_id; + } + } + } + } + }); + if redraw != 0 { + self.disconect_state.connected_shortcuts = None; + self.settings.blacklisted_games.push(redraw); + } + } + Err(msg) => { + ui.label(&*msg); + } + } + } +} + +fn set_scroll_style(ui: &mut egui::Ui) { + let mut scroll_style = ui.style_mut(); + scroll_style.visuals.extreme_bg_color = BACKGROUND_COLOR; + scroll_style.visuals.widgets.inactive.bg_fill = EXTRA_BACKGROUND_COLOR; + scroll_style.visuals.widgets.active.bg_fill = EXTRA_BACKGROUND_COLOR; + scroll_style.visuals.widgets.hovered.bg_fill = EXTRA_BACKGROUND_COLOR; +} diff --git a/src/ui/ui_image_download.rs b/src/ui/ui_image_download.rs index 5653a58..1d25471 100644 --- a/src/ui/ui_image_download.rs +++ b/src/ui/ui_image_download.rs @@ -149,13 +149,12 @@ impl MyEguiApp { return value; } } else if let Some(image_type) = state.image_type_selected.as_ref() { - if let Some(action) = self.render_possible_images(ui, image_type, state) { - return action; - } - } else if let Some(action) = render_shortcut_images(ui, state) { + if let Some(action) = self.render_possible_images(ui, image_type, state) { return action; + } + } else if let Some(action) = render_shortcut_images(ui, state) { + return action; } - } else { let is_shortcut = state.game_mode.is_shortcuts(); if ui @@ -176,9 +175,8 @@ impl MyEguiApp { return action; } } else if let Some(action) = render_steam_game_select(ui, state) { - return action; + return action; } - } UserAction::NoAction } @@ -660,13 +658,10 @@ fn render_shortcut_images(ui: &mut egui::Ui, state: &ImageSelectState) -> Option for image_type in ImageType::all() { ui.label(image_type.name()); let (_path, key) = shortcut.key(image_type, Path::new(&user_path)); - let texture = state - .image_handles - .get(&key) - .and_then(|k| match k.value() { - TextureState::Loaded(texture) => Some(texture.clone()), - _ => None, - }); + let texture = state.image_handles.get(&key).and_then(|k| match k.value() { + TextureState::Loaded(texture) => Some(texture.clone()), + _ => None, + }); let clicked = render_thumbnail(ui, texture); if clicked { return Some(UserAction::ImageTypeSelected(*image_type)); diff --git a/src/ui/ui_import_games.rs b/src/ui/ui_import_games.rs index 228bafe..5162a5a 100644 --- a/src/ui/ui_import_games.rs +++ b/src/ui/ui_import_games.rs @@ -9,7 +9,7 @@ use crate::sync; use crate::sync::{download_images, SyncProgress}; -use super::{ImageSelectState, backup_shortcuts}; +use super::{backup_shortcuts, ImageSelectState}; use super::{ ui_colors::{BACKGROUND_COLOR, EXTRA_BACKGROUND_COLOR}, MyEguiApp, @@ -42,9 +42,6 @@ impl FetcStatus { } impl MyEguiApp { - - - pub(crate) fn render_import_games(&mut self, ui: &mut egui::Ui) { ui.heading("Import Games"); diff --git a/src/ui/ui_settings.rs b/src/ui/ui_settings.rs index c27ecf2..569054d 100644 --- a/src/ui/ui_settings.rs +++ b/src/ui/ui_settings.rs @@ -8,7 +8,7 @@ use super::{ ui_colors::{BACKGROUND_COLOR, EXTRA_BACKGROUND_COLOR}, MyEguiApp, }; -const SECTION_SPACING: f32 = 25.0; +pub const SECTION_SPACING: f32 = 25.0; const VERSION: &str = env!("CARGO_PKG_VERSION"); impl MyEguiApp { @@ -49,6 +49,9 @@ impl MyEguiApp { { self.render_amazon_settings(ui); } + + self.render_disconnect(ui); + ui.add_space(SECTION_SPACING); ui.label(format!("Version: {}", VERSION)); }); diff --git a/src/ui/uiapp.rs b/src/ui/uiapp.rs index 41b532d..75418da 100644 --- a/src/ui/uiapp.rs +++ b/src/ui/uiapp.rs @@ -17,7 +17,7 @@ use super::{ }, ui_images::{get_import_image, get_logo, get_logo_icon}, ui_import_games::FetcStatus, - BackupState, ImageSelectState, + BackupState, DiconnectState, ImageSelectState, }; const SECTION_SPACING: f32 = 25.0; @@ -39,6 +39,7 @@ pub struct MyEguiApp { pub(crate) heroic_games: Option>, pub(crate) image_selected_state: ImageSelectState, pub(crate) backup_state: BackupState, + pub(crate) disconect_state: DiconnectState, } impl MyEguiApp { @@ -55,6 +56,7 @@ impl MyEguiApp { heroic_games: None, image_selected_state: ImageSelectState::default(), backup_state: BackupState::default(), + disconect_state: DiconnectState::default(), } } } @@ -65,6 +67,7 @@ enum Menues { Settings, Images, Backup, + Disconnect, } impl Default for Menues { @@ -106,6 +109,11 @@ impl App for MyEguiApp { .selectable_value(&mut self.selected_menu, Menues::Backup, "Backup") .changed(); + changed = changed + || ui + .selectable_value(&mut self.selected_menu, Menues::Disconnect, "Disconnect") + .changed(); + if changed { self.backup_state.available_backups = None; } @@ -172,6 +180,9 @@ impl App for MyEguiApp { Menues::Backup => { self.render_backup(ui); } + Menues::Disconnect => { + self.render_disconnect(ui); + } }; }); } diff --git a/src/uplay/game.rs b/src/uplay/game.rs index efd2ad0..b2b6243 100644 --- a/src/uplay/game.rs +++ b/src/uplay/game.rs @@ -1,4 +1,4 @@ -use std::path::{PathBuf, Path}; +use std::path::{Path, PathBuf}; use steam_shortcuts_util::shortcut::{Shortcut, ShortcutOwned}; @@ -7,14 +7,18 @@ pub(crate) struct Game { pub(crate) name: String, pub(crate) icon: String, pub(crate) id: String, - pub(crate) launcher: PathBuf + pub(crate) launcher: PathBuf, } impl From for ShortcutOwned { fn from(game: Game) -> Self { let launch = format!("\"uplay://launch/{}/0\"", game.id); - let start_dir = game.launcher.parent().unwrap_or_else(|| {Path::new("")}).to_string_lossy(); - let exe = format!("\"{}\"",game.launcher.to_string_lossy()); + let start_dir = game + .launcher + .parent() + .unwrap_or_else(|| Path::new("")) + .to_string_lossy(); + let exe = format!("\"{}\"", game.launcher.to_string_lossy()); Shortcut::new("0", &game.name, &exe, &start_dir, &game.icon, "", &launch).to_owned() } }