From bcf1b313008b6ac7b4a7c8d4c1d92ae63f0cb7b6 Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Tue, 10 Feb 2026 12:09:45 +0530 Subject: [PATCH 01/30] Dev: start work on ezshortcut --- Cargo.lock | 8 +++ Cargo.toml | 9 ++-- crates/ezshortcut/Cargo.toml | 8 +++ crates/ezshortcut/src/async_impl/mod.rs | 17 ++++++ crates/ezshortcut/src/async_impl/stub.rs | 9 ++++ crates/ezshortcut/src/async_impl/unix.rs | 59 +++++++++++++++++++++ crates/ezshortcut/src/lib.rs | 66 ++++++++++++++++++++++++ 7 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 crates/ezshortcut/Cargo.toml create mode 100644 crates/ezshortcut/src/async_impl/mod.rs create mode 100644 crates/ezshortcut/src/async_impl/stub.rs create mode 100644 crates/ezshortcut/src/async_impl/unix.rs create mode 100644 crates/ezshortcut/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index c7167e288..5967438c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1459,6 +1459,14 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "ezshortcut" +version = "0.1.0" +dependencies = [ + "cfg-if", + "tokio", +] + [[package]] name = "fast-srgb8" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index 1f65c621a..59c920aa3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,16 @@ [workspace] members = [ - "quantum_launcher", + "crates/ql_core", "crates/ql_instances", "crates/ql_mod_manager", - "crates/ql_core", "crates/ql_servers", - "crates/ql_java_handler", "crates/ql_packager", + # Binaries + "quantum_launcher", "tests", + # Utility Libraries + "crates/ql_java_handler", + "crates/ezshortcut", ] default-members = ["quantum_launcher"] resolver = "2" diff --git a/crates/ezshortcut/Cargo.toml b/crates/ezshortcut/Cargo.toml new file mode 100644 index 000000000..9237ed9fa --- /dev/null +++ b/crates/ezshortcut/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "ezshortcut" +version = "0.1.0" +edition = "2021" + +[dependencies] +cfg-if.workspace = true +tokio.workspace = true diff --git a/crates/ezshortcut/src/async_impl/mod.rs b/crates/ezshortcut/src/async_impl/mod.rs new file mode 100644 index 000000000..6178ff5c1 --- /dev/null +++ b/crates/ezshortcut/src/async_impl/mod.rs @@ -0,0 +1,17 @@ +use cfg_if::cfg_if; + +cfg_if!( + if #[cfg(target_os = "windows")] { + mod stub; + pub use stub::*; + } else if #[cfg(target_os = "macos")] { + mod stub; + pub use stub::*; + } else if #[cfg(target_family = "unix")] { + mod unix; + pub use unix::*; + } else { + mod stub; + pub use stub::*; + } +); diff --git a/crates/ezshortcut/src/async_impl/stub.rs b/crates/ezshortcut/src/async_impl/stub.rs new file mode 100644 index 000000000..3973f36a3 --- /dev/null +++ b/crates/ezshortcut/src/async_impl/stub.rs @@ -0,0 +1,9 @@ +use crate::Shortcut; +use std::path::Path; + +pub async fn create(shortcut: &Shortcut, path: impl AsRef) -> std::io::Result<()> { + Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "Shortcuts aren't supported on this platform", + )) +} diff --git a/crates/ezshortcut/src/async_impl/unix.rs b/crates/ezshortcut/src/async_impl/unix.rs new file mode 100644 index 000000000..b7e00cecc --- /dev/null +++ b/crates/ezshortcut/src/async_impl/unix.rs @@ -0,0 +1,59 @@ +use std::{os::unix::fs::PermissionsExt, path::Path}; + +use tokio::fs; + +use crate::{Shortcut, make_filename_safe}; + +pub async fn create(shortcut: &Shortcut, path: impl AsRef) -> std::io::Result<()> { + let path = path.as_ref(); + create_inner(shortcut, path).await?; + + Ok(()) +} + +async fn create_inner(shortcut: &Shortcut, path: &Path) -> Result<(), std::io::Error> { + let content = format!( + r"[Desktop Entry] +Version=1.0 +Type=Application +Name={name} +Comment={description} +Exec={exec} +{icon} +Terminal=false +Categories=Game;", + name = shortcut.name, + description = shortcut.description, + exec = shortcut.exec, + icon = shortcut + .icon + .as_deref() + .map(|n| format!("Icon={n}")) + .unwrap_or_default() + ); + + match fs::metadata(path).await { + Ok(n) => { + if n.is_dir() { + write_file( + &path.join(format!( + "{}.desktop", + make_filename_safe(&shortcut.name, true) + )), + content, + ) + .await?; + } + } + _ => write_file(path, content).await?, + }; + Ok(()) +} + +async fn write_file(path: &Path, content: String) -> std::io::Result<()> { + fs::write(path, content).await?; + let mut perms = fs::metadata(path).await?.permissions(); + perms.set_mode(0o755); + fs::set_permissions(path, perms).await?; + Ok(()) +} diff --git a/crates/ezshortcut/src/lib.rs b/crates/ezshortcut/src/lib.rs new file mode 100644 index 000000000..682447892 --- /dev/null +++ b/crates/ezshortcut/src/lib.rs @@ -0,0 +1,66 @@ +use std::path::Path; + +mod async_impl; + +pub struct Shortcut { + pub name: String, + pub description: String, + pub exec: String, + pub icon: Option, +} + +impl Shortcut { + pub async fn generate(&self, path: &Path) -> std::io::Result<()> { + async_impl::create(self, path).await + } +} + +fn make_filename_safe(input: &str, remove_spaces: bool) -> String { + let mut out = String::with_capacity(input.len()); + + for c in input.chars() { + match c { + '-' | '_' | '.' => out.push(c), + ' ' | '/' | '\\' | '|' | ':' => out.push('_'), + '<' | '>' | '"' | '\'' | '?' | '*' => continue, + c if c.is_control() => continue, + c if c.is_whitespace() => out.push(if remove_spaces { '_' } else { ' ' }), + + _ => out.push(c), + } + } + + // Collapse multiple underscores + let mut collapsed = String::with_capacity(out.len()); + let mut last_was_underscore = false; + + for c in out.chars() { + if c == '_' { + if !last_was_underscore { + collapsed.push(c); + last_was_underscore = true; + } + } else { + collapsed.push(c); + last_was_underscore = false; + } + } + + // Trim leading/trailing spaces, dots, and underscores (Windows edge cases) + let trimmed = collapsed + .trim_matches(|c: char| c == ' ' || c == '.' || c == '_') + .to_string(); + + // Avoid reserved Windows filenames + let upper = trimmed.to_ascii_uppercase(); + let reserved = [ + "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", + "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", + ]; + + if reserved.contains(&upper.as_str()) || trimmed.is_empty() { + "_".to_string() + } else { + trimmed + } +} From 374932dc0b68c484f5a3f58e7f2995175b5dedf0 Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Tue, 10 Feb 2026 12:22:58 +0530 Subject: [PATCH 02/30] Dev: Kinda start work on platform impls, add get_menu_path function --- Cargo.lock | 1 + Cargo.toml | 1 + crates/ezshortcut/Cargo.toml | 1 + crates/ezshortcut/src/async_impl/macos.rs | 1 + crates/ezshortcut/src/async_impl/mod.rs | 8 ++++---- crates/ezshortcut/src/async_impl/unix.rs | 2 +- crates/ezshortcut/src/async_impl/windows.rs | 1 + crates/ezshortcut/src/lib.rs | 3 +++ crates/ezshortcut/src/sync_impl/macos.rs | 5 +++++ crates/ezshortcut/src/sync_impl/mod.rs | 17 +++++++++++++++++ crates/ezshortcut/src/sync_impl/stub.rs | 5 +++++ crates/ezshortcut/src/sync_impl/unix.rs | 9 +++++++++ crates/ezshortcut/src/sync_impl/windows.rs | 5 +++++ crates/ql_core/Cargo.toml | 2 +- 14 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 crates/ezshortcut/src/async_impl/macos.rs create mode 100644 crates/ezshortcut/src/async_impl/windows.rs create mode 100644 crates/ezshortcut/src/sync_impl/macos.rs create mode 100644 crates/ezshortcut/src/sync_impl/mod.rs create mode 100644 crates/ezshortcut/src/sync_impl/stub.rs create mode 100644 crates/ezshortcut/src/sync_impl/unix.rs create mode 100644 crates/ezshortcut/src/sync_impl/windows.rs diff --git a/Cargo.lock b/Cargo.lock index 5967438c2..76ace3145 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1464,6 +1464,7 @@ name = "ezshortcut" version = "0.1.0" dependencies = [ "cfg-if", + "dirs", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index 59c920aa3..3ab164e99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ resolver = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" semver = "1" +dirs = "6" reqwest = { version = "0.12", default-features = false, features = [ "json", diff --git a/crates/ezshortcut/Cargo.toml b/crates/ezshortcut/Cargo.toml index 9237ed9fa..13e3a7fa9 100644 --- a/crates/ezshortcut/Cargo.toml +++ b/crates/ezshortcut/Cargo.toml @@ -6,3 +6,4 @@ edition = "2021" [dependencies] cfg-if.workspace = true tokio.workspace = true +dirs.workspace = true diff --git a/crates/ezshortcut/src/async_impl/macos.rs b/crates/ezshortcut/src/async_impl/macos.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/crates/ezshortcut/src/async_impl/macos.rs @@ -0,0 +1 @@ + diff --git a/crates/ezshortcut/src/async_impl/mod.rs b/crates/ezshortcut/src/async_impl/mod.rs index 6178ff5c1..66befdf77 100644 --- a/crates/ezshortcut/src/async_impl/mod.rs +++ b/crates/ezshortcut/src/async_impl/mod.rs @@ -2,11 +2,11 @@ use cfg_if::cfg_if; cfg_if!( if #[cfg(target_os = "windows")] { - mod stub; - pub use stub::*; + mod windows; + pub use windows::*; } else if #[cfg(target_os = "macos")] { - mod stub; - pub use stub::*; + mod macos; + pub use macos::*; } else if #[cfg(target_family = "unix")] { mod unix; pub use unix::*; diff --git a/crates/ezshortcut/src/async_impl/unix.rs b/crates/ezshortcut/src/async_impl/unix.rs index b7e00cecc..7709e00df 100644 --- a/crates/ezshortcut/src/async_impl/unix.rs +++ b/crates/ezshortcut/src/async_impl/unix.rs @@ -2,7 +2,7 @@ use std::{os::unix::fs::PermissionsExt, path::Path}; use tokio::fs; -use crate::{Shortcut, make_filename_safe}; +use crate::{make_filename_safe, Shortcut}; pub async fn create(shortcut: &Shortcut, path: impl AsRef) -> std::io::Result<()> { let path = path.as_ref(); diff --git a/crates/ezshortcut/src/async_impl/windows.rs b/crates/ezshortcut/src/async_impl/windows.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/crates/ezshortcut/src/async_impl/windows.rs @@ -0,0 +1 @@ + diff --git a/crates/ezshortcut/src/lib.rs b/crates/ezshortcut/src/lib.rs index 682447892..ef3e32987 100644 --- a/crates/ezshortcut/src/lib.rs +++ b/crates/ezshortcut/src/lib.rs @@ -1,6 +1,9 @@ use std::path::Path; mod async_impl; +mod sync_impl; + +pub use sync_impl::get_menu_path; pub struct Shortcut { pub name: String, diff --git a/crates/ezshortcut/src/sync_impl/macos.rs b/crates/ezshortcut/src/sync_impl/macos.rs new file mode 100644 index 000000000..60cc1c0e0 --- /dev/null +++ b/crates/ezshortcut/src/sync_impl/macos.rs @@ -0,0 +1,5 @@ +use std::path::PathBuf; + +pub fn get_menu_path() -> Option { + dirs::home_dir().map(|h| h.join("Applications")) +} diff --git a/crates/ezshortcut/src/sync_impl/mod.rs b/crates/ezshortcut/src/sync_impl/mod.rs new file mode 100644 index 000000000..66befdf77 --- /dev/null +++ b/crates/ezshortcut/src/sync_impl/mod.rs @@ -0,0 +1,17 @@ +use cfg_if::cfg_if; + +cfg_if!( + if #[cfg(target_os = "windows")] { + mod windows; + pub use windows::*; + } else if #[cfg(target_os = "macos")] { + mod macos; + pub use macos::*; + } else if #[cfg(target_family = "unix")] { + mod unix; + pub use unix::*; + } else { + mod stub; + pub use stub::*; + } +); diff --git a/crates/ezshortcut/src/sync_impl/stub.rs b/crates/ezshortcut/src/sync_impl/stub.rs new file mode 100644 index 000000000..174bb035b --- /dev/null +++ b/crates/ezshortcut/src/sync_impl/stub.rs @@ -0,0 +1,5 @@ +use std::path::PathBuf; + +pub fn get_menu_path() -> Option { + None +} diff --git a/crates/ezshortcut/src/sync_impl/unix.rs b/crates/ezshortcut/src/sync_impl/unix.rs new file mode 100644 index 000000000..e742f1137 --- /dev/null +++ b/crates/ezshortcut/src/sync_impl/unix.rs @@ -0,0 +1,9 @@ +use std::path::PathBuf; + +pub fn get_menu_path() -> Option { + if let Ok(dir) = std::env::var("XDG_DATA_HOME") { + Some(PathBuf::from(dir).join("applications")) + } else { + dirs::home_dir().map(|h| h.join(".local/share/applications")) + } +} diff --git a/crates/ezshortcut/src/sync_impl/windows.rs b/crates/ezshortcut/src/sync_impl/windows.rs new file mode 100644 index 000000000..ac6c4bbf6 --- /dev/null +++ b/crates/ezshortcut/src/sync_impl/windows.rs @@ -0,0 +1,5 @@ +use std::path::PathBuf; + +pub fn get_menu_path() -> Option { + dirs::data_dir().map(|d| d.join("Microsoft/Windows/Start Menu/Programs")) +} diff --git a/crates/ql_core/Cargo.toml b/crates/ql_core/Cargo.toml index 9ad58f424..94964238c 100644 --- a/crates/ql_core/Cargo.toml +++ b/crates/ql_core/Cargo.toml @@ -11,7 +11,7 @@ simulate_linux_arm32 = [] [dependencies] chrono.workspace = true -dirs = "6" +dirs.workspace = true futures.workspace = true owo-colors.workspace = true cfg-if.workspace = true From 79744cbcd7b159069313262c6f4c7e56f072ac0d Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Tue, 10 Feb 2026 14:07:38 +0530 Subject: [PATCH 03/30] Dev: Add UI for shortcut creation --- Cargo.lock | 1 + crates/ezshortcut/src/async_impl/unix.rs | 13 +- quantum_launcher/Cargo.toml | 1 + quantum_launcher/src/menu_renderer/launch.rs | 9 +- quantum_launcher/src/menu_renderer/mod.rs | 170 +++++++++++++----- .../src/message_handler/iced_event.rs | 1 + quantum_launcher/src/message_update/mod.rs | 2 + .../src/message_update/shortcuts.rs | 48 +++++ quantum_launcher/src/state/menu.rs | 8 + quantum_launcher/src/state/message.rs | 10 ++ quantum_launcher/src/tick.rs | 1 + quantum_launcher/src/update.rs | 1 + quantum_launcher/src/view.rs | 1 + 13 files changed, 217 insertions(+), 49 deletions(-) create mode 100644 quantum_launcher/src/message_update/shortcuts.rs diff --git a/Cargo.lock b/Cargo.lock index 76ace3145..2595c41bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4076,6 +4076,7 @@ dependencies = [ "clap", "dark-light", "embed-resource", + "ezshortcut", "frostmark", "iced", "notify", diff --git a/crates/ezshortcut/src/async_impl/unix.rs b/crates/ezshortcut/src/async_impl/unix.rs index 7709e00df..cf1a27b33 100644 --- a/crates/ezshortcut/src/async_impl/unix.rs +++ b/crates/ezshortcut/src/async_impl/unix.rs @@ -12,23 +12,26 @@ pub async fn create(shortcut: &Shortcut, path: impl AsRef) -> std::io::Res } async fn create_inner(shortcut: &Shortcut, path: &Path) -> Result<(), std::io::Error> { + let desc = shortcut.description.trim(); let content = format!( r"[Desktop Entry] Version=1.0 Type=Application Name={name} -Comment={description} -Exec={exec} -{icon} +{icon}{description}Exec={exec} Terminal=false Categories=Game;", name = shortcut.name, - description = shortcut.description, + description = if desc.is_empty() { + String::new() + } else { + format!("Comment={desc}\n") + }, exec = shortcut.exec, icon = shortcut .icon .as_deref() - .map(|n| format!("Icon={n}")) + .map(|n| format!("Icon={n}\n")) .unwrap_or_default() ); diff --git a/quantum_launcher/Cargo.toml b/quantum_launcher/Cargo.toml index c7bbc0691..583a53b05 100644 --- a/quantum_launcher/Cargo.toml +++ b/quantum_launcher/Cargo.toml @@ -37,6 +37,7 @@ ql_mod_manager.path = "../crates/ql_mod_manager" ql_core.path = "../crates/ql_core" ql_servers.path = "../crates/ql_servers" ql_packager.path = "../crates/ql_packager" +ezshortcut.path = "../crates/ezshortcut" # For the GUI iced = { version = "0.13", default-features = false, features = [ diff --git a/quantum_launcher/src/menu_renderer/launch.rs b/quantum_launcher/src/menu_renderer/launch.rs index 76ac42768..c8680ff9e 100644 --- a/quantum_launcher/src/menu_renderer/launch.rs +++ b/quantum_launcher/src/menu_renderer/launch.rs @@ -9,7 +9,9 @@ use ql_core::{InstanceSelection, LAUNCHER_VERSION_NAME}; use crate::cli::EXPERIMENTAL_SERVERS; use crate::menu_renderer::onboarding::x86_warning; use crate::menu_renderer::{ctx_button, ctxbox, tsubtitle, underline, underline_maybe, FONT_MONO}; -use crate::state::{GameLogMessage, InstanceNotes, LaunchModal, NotesMessage, WindowMessage}; +use crate::state::{ + GameLogMessage, InstanceNotes, LaunchModal, NotesMessage, ShortcutMessage, WindowMessage, +}; use crate::{ icons, menu_renderer::DISCORD, @@ -115,7 +117,8 @@ impl Launcher { ctxbox( widget::column![ ctx_button("Export Instance").on_press(Message::ExportInstanceOpen), - ctx_button("Create Shortcut").on_press(Message::Nothing), + ctx_button("Create Shortcut") + .on_press(Message::Shortcut(ShortcutMessage::Open)), ] .spacing(5) ) @@ -199,7 +202,7 @@ impl Launcher { .push( widget::row![ widget::button(icons::lines_s(10)) - .padding([4, 8]) + .padding([5, 8]) .on_press(Message::MModal(Some(LaunchModal::InstanceOptions))), widget::button( widget::row![ diff --git a/quantum_launcher/src/menu_renderer/mod.rs b/quantum_launcher/src/menu_renderer/mod.rs index c92d7b38a..7d1955a81 100644 --- a/quantum_launcher/src/menu_renderer/mod.rs +++ b/quantum_launcher/src/menu_renderer/mod.rs @@ -1,8 +1,12 @@ -use iced::widget::tooltip::Position; -use iced::{widget, Alignment, Length}; +use cfg_if::cfg_if; +use iced::{ + widget::{self, column, row, tooltip::Position}, + Alignment, Length, +}; use ql_core::{Progress, WEBSITE}; use ql_instances::auth::AccountType; +use crate::state::{MenuShortcut, ShortcutMessage}; use crate::stylesheet::styles::{LauncherThemeLightness, BORDER_RADIUS, BORDER_WIDTH}; use crate::{ config::LauncherConfig, @@ -102,8 +106,8 @@ pub fn underline<'a>( color: Color, ) -> widget::Stack<'a, Message, LauncherTheme> { widget::stack!( - widget::column![e.into()], - widget::column![ + column![e.into()], + column![ widget::vertical_space(), widget::horizontal_rule(1).style(move |t: &LauncherTheme| t.style_rule(color, 1)), widget::Space::with_height(1), @@ -120,7 +124,7 @@ pub fn underline_maybe<'a>(e: impl Into>, color: Color, un: bool) -> } pub fn center_x<'a>(e: impl Into>) -> widget::Row<'a, Message, LauncherTheme> { - widget::row![ + row![ widget::horizontal_space(), e.into(), widget::horizontal_space(), @@ -153,7 +157,7 @@ pub fn subbutton_with_icon<'a>( text: &'a str, ) -> widget::Button<'a, Message, LauncherTheme> { widget::button( - widget::row![icon.into()] + row![icon.into()] .push_maybe((!text.is_empty()).then_some(widget::text(text).size(12))) .align_y(Alignment::Center) .spacing(8) @@ -168,7 +172,7 @@ pub fn button_with_icon<'a>( size: u16, ) -> widget::Button<'a, Message, LauncherTheme> { widget::button( - widget::row![icon.into()] + row![icon.into()] .push_maybe((!text.is_empty()).then_some(widget::text(text).size(size))) .align_y(Alignment::Center) .spacing(size as f32 / 1.6), @@ -209,7 +213,7 @@ fn sidebar<'a>( children: impl IntoIterator>, ) -> widget::Container<'a, Message, LauncherTheme> { widget::container( - widget::column![ + column![ widget::Column::new() .push_maybe(header) .padding(PADDING_NOT_BOTTOM), @@ -231,11 +235,11 @@ fn dots(tick_timer: usize) -> String { impl MenuLauncherUpdate { pub fn view(&'_ self) -> Element<'_> { if let Some(progress) = &self.progress { - return widget::column!("Updating QuantumLauncher...", progress.view()) + return column!["Updating QuantumLauncher...", progress.view()] .padding(10) .into(); } - widget::column!( + column![ "A new launcher update has been found! Do you want to download it?", widget::Row::new() .push_maybe((!cfg!(target_os = "macos")).then_some( @@ -252,13 +256,13 @@ impl MenuLauncherUpdate { .push(button_with_icon(icons::globe(), "Open Website", 16) .on_press(Message::CoreOpenLink(WEBSITE.to_owned()))) .spacing(5).wrap(), - ) + ] // WARN: Auto update configurations .push_maybe(cfg!(target_os = "linux").then_some( - widget::column!( + column![ "If you installed this launcher from a package manager/store (flatpak/apt/dnf/pacman/..) then update from there", "If you downloaded it from website then it's fine." - ) + ] )) .padding(10) .spacing(10) @@ -286,11 +290,11 @@ pub fn get_mode_selector(config: &LauncherConfig) -> Element<'static> { }; if *n == theme { - widget::container(widget::row![icon.style(td), name].spacing(5)) + widget::container(row![icon.style(td), name].spacing(5)) .padding(PADDING) .into() } else { - widget::button(widget::row![icon, name].spacing(5)) + widget::button(row![icon, name].spacing(5)) .on_press(Message::LauncherSettings( LauncherSettingsMessage::ThemePicked(*n), )) @@ -313,21 +317,15 @@ fn back_to_launch_screen(is_server: Option, message: Option) -> Me impl ProgressBar { pub fn view(&'_ self) -> widget::Column<'_, Message, LauncherTheme> { let total = T::total(); - if let Some(message) = &self.message { - widget::column!( - widget::progress_bar(0.0..=total, self.num), - widget::text(message) - ) - } else { - widget::column!(widget::progress_bar(0.0..=total, self.num)) - } - .spacing(10) + column![widget::progress_bar(0.0..=total, self.num)] + .push_maybe(self.message.as_deref().map(widget::text)) + .spacing(10) } } impl MenuCurseforgeManualDownload { pub fn view(&'_ self) -> Element<'_> { - widget::column![ + column![ "Some Curseforge mods have blocked this launcher!\nYou need to manually download the files and add them to your mods", widget::scrollable( @@ -339,7 +337,7 @@ impl MenuCurseforgeManualDownload { entry.file_id ); - widget::row![ + row![ widget::button(widget::text("Open link").size(14)).on_press(Message::CoreOpenLink(url)), widget::text(&entry.name) .shaping(widget::text::Shaping::Advanced) @@ -354,7 +352,7 @@ impl MenuCurseforgeManualDownload { .style(LauncherTheme::style_scrollable_flat_extra_dark), "Warning: Ignoring this may lead to crashes!", - widget::row![ + row![ widget::button(widget::text("+ Select above downloaded files").size(14)).on_press(Message::ManageMods(ManageModsMessage::AddFile(self.delete_mods))), widget::button(widget::text("Continue").size(14)).on_press(if self.is_store { Message::InstallMods(InstallModsMessage::Open) @@ -376,7 +374,7 @@ impl MenuCurseforgeManualDownload { impl MenuLicense { pub fn view(&'_ self) -> Element<'_> { - widget::row![ + row![ sidebar( "MenuLicense:sidebar", Some( @@ -409,13 +407,103 @@ impl MenuLicense { } } +impl MenuShortcut { + pub fn view(&self) -> Element<'_> { + cfg_if!(if #[cfg(target_os = "windows")] { + const MENU_NAME: &str = "the Start Menu"; + const IS_UNIX: bool = false; + } else if #[cfg(target_os = "macos")] { + const MENU_NAME: &str = "Applications"; + const IS_UNIX: bool = false; + } else { + const MENU_NAME: &str = "the Applications Menu"; + const IS_UNIX: bool = true; + }); + + row![ + widget::scrollable( + column![ + row![ + back_button().on_press(Message::MScreenOpen { + message: None, + clear_selection: false, + is_server: None + }), + widget::text("Create Launch Shortcut").size(20), + ] + .align_y(Alignment::Center) + .spacing(16), + widget::text!("Launch this instance directly from your Desktop or {MENU_NAME} without opening the launcher") + .size(14) + .style(tsubtitle), + column![row![ + "Name:", + widget::text_input("(Required)", &self.shortcut.name) + .size(14) + .on_input(|n| Message::Shortcut(ShortcutMessage::EditName(n))) + ].spacing(10).align_y(Alignment::Center)] + .spacing(5) + .push_maybe( + IS_UNIX.then_some( + row![ + "Description:", + widget::text_input("Leave blank for none", &self.shortcut.description) + .size(14) + .on_input(|n| Message::Shortcut(ShortcutMessage::EditDescription(n))) + ].align_y(Alignment::Center).spacing(10) + ) + ), + column![ + widget::checkbox(format!("Add to {MENU_NAME}"), self.add_to_menu) + .on_toggle(|t| Message::Shortcut(ShortcutMessage::ToggleAddToMenu(t))) + .size(12) + .text_size(12), + widget::checkbox(format!("Add to Desktop"), self.add_to_desktop) + .on_toggle(|t| Message::Shortcut(ShortcutMessage::ToggleAddToDesktop(t))) + .size(12) + .text_size(12), + button_with_icon(icons::checkmark_s(14), "Create Shortcut", 14).on_press(Message::Nothing), + ] + .spacing(5), + column![ + widget::text("Or save a shortcut file to use anywhere") + .size(14) + .style(tsubtitle), + button_with_icon(icons::floppydisk_s(14), "Export Shortcut File...", 14).on_press(Message::Nothing), + ] + .spacing(5) + ] + .width(Length::Fill) + .padding(16) + .spacing(16) + ) + .style(|t: &LauncherTheme, s| t.style_scrollable_flat_dark(s)), + widget::scrollable( + column![ + widget::text("Existing Shortcuts").size(20), + widget::text( + "TODO: Store and show shortcut info here, and have options to delete them" + ) + .size(12) + ] + .padding(16) + .spacing(10) + ) + .height(Length::Fill) + .width(200) + .style(|t: &LauncherTheme, s| t.style_scrollable_flat_extra_dark(s)) + ] + .into() + } +} + pub fn view_account_login<'a>() -> Element<'a> { - widget::column![ + column![ back_button().on_press(back_to_launch_screen(None, None)), widget::vertical_space(), - widget::row![ + row![ widget::horizontal_space(), - widget::column![ + column![ widget::text("Login").size(20), widget::button("Login with Microsoft").on_press(Message::Account( AccountMessage::OpenMenu { @@ -449,9 +537,9 @@ pub fn view_account_login<'a>() -> Element<'a> { pub fn view_error(error: &'_ str) -> Element<'_> { widget::scrollable( - widget::column!( + column![ widget::text!("Error: {error}"), - widget::row![ + row![ widget::button("Back").on_press(back_to_launch_screen(None, None)), widget::button("Copy Error").on_press(Message::CoreCopyError), widget::button("Copy Error + Log").on_press(Message::CoreCopyLog), @@ -460,7 +548,7 @@ pub fn view_error(error: &'_ str) -> Element<'_> { ] .spacing(5) .wrap() - ) + ] .padding(10) .spacing(10), ) @@ -471,9 +559,9 @@ pub fn view_error(error: &'_ str) -> Element<'_> { } pub fn view_log_upload_result(url: &'_ str, is_server: bool) -> Element<'_> { - widget::column![ + column![ back_button().on_press(back_to_launch_screen(Some(is_server), None)), - widget::column![ + column![ widget::vertical_space(), widget::text(format!( "{} log uploaded successfully!", @@ -483,7 +571,7 @@ pub fn view_log_upload_result(url: &'_ str, is_server: bool) -> Element<'_> { widget::text("Your log has been uploaded to mclo.gs. You can share the link below:") .size(14), widget::container( - widget::row![ + row![ widget::text(url).font(FONT_MONO), widget::button("Copy").on_press(Message::CoreCopyText(url.to_string())), widget::button("Open").on_press(Message::CoreOpenLink(url.to_string())) @@ -513,13 +601,13 @@ pub fn view_confirm<'a>( color: Some(iced::Color::WHITE), }; - widget::column![ + column![ widget::vertical_space(), widget::text!("Are you sure you want to {msg1}?").size(20), msg2, - widget::row![ + row![ widget::button( - widget::row![ + row![ icons::cross().style(t_white), widget::text("No").style(t_white) ] @@ -532,7 +620,7 @@ pub fn view_confirm<'a>( style_button_color(status, (0x72, 0x22, 0x24), (0x9f, 0x2c, 0x2f)) }), widget::button( - widget::row![ + row![ icons::deselectall().style(t_white), widget::text("Yes").style(t_white) ] diff --git a/quantum_launcher/src/message_handler/iced_event.rs b/quantum_launcher/src/message_handler/iced_event.rs index af398877e..86568b4f9 100644 --- a/quantum_launcher/src/message_handler/iced_event.rs +++ b/quantum_launcher/src/message_handler/iced_event.rs @@ -315,6 +315,7 @@ impl Launcher { | State::LoginAlternate(MenuLoginAlternate { is_loading: false, .. }) + | State::CreateShortcut(_) | State::Welcome(_) => { ret_to_main_screen = true; } diff --git a/quantum_launcher/src/message_update/mod.rs b/quantum_launcher/src/message_update/mod.rs index a5db90471..0d7d6bbd8 100644 --- a/quantum_launcher/src/message_update/mod.rs +++ b/quantum_launcher/src/message_update/mod.rs @@ -29,6 +29,8 @@ use crate::{ }, }; +mod shortcuts; + pub const MSG_RESIZE: &str = "Resize your window to apply the changes."; impl Launcher { diff --git a/quantum_launcher/src/message_update/shortcuts.rs b/quantum_launcher/src/message_update/shortcuts.rs new file mode 100644 index 000000000..94d003190 --- /dev/null +++ b/quantum_launcher/src/message_update/shortcuts.rs @@ -0,0 +1,48 @@ +use ezshortcut::Shortcut; +use iced::Task; + +use crate::state::{Launcher, MenuShortcut, Message, ShortcutMessage, State}; + +macro_rules! iflet { + ($m:ident, $s:expr, $b:block) => {{ + if let State::CreateShortcut($m) = &mut $s { + $b + } + }}; +} + +impl Launcher { + pub fn update_shortcut(&mut self, msg: ShortcutMessage) -> Task { + match msg { + ShortcutMessage::Open => { + self.state = State::CreateShortcut(MenuShortcut { + shortcut: Shortcut { + name: self.instance().get_name().to_owned(), + description: "".to_owned(), + exec: String::new(), + icon: None, + }, + add_to_menu: true, + add_to_desktop: false, + }) + } + ShortcutMessage::ToggleAddToMenu(t) => iflet!(menu, self.state, { + if t || menu.add_to_desktop { + menu.add_to_menu = t; + } + }), + ShortcutMessage::ToggleAddToDesktop(t) => iflet!(menu, self.state, { + if t || menu.add_to_menu { + menu.add_to_desktop = t; + } + }), + ShortcutMessage::EditName(name) => iflet!(menu, self.state, { + menu.shortcut.name = name; + }), + ShortcutMessage::EditDescription(desc) => iflet!(menu, self.state, { + menu.shortcut.description = desc; + }), + } + Task::none() + } +} diff --git a/quantum_launcher/src/state/menu.rs b/quantum_launcher/src/state/menu.rs index 914629816..ec1faffb1 100644 --- a/quantum_launcher/src/state/menu.rs +++ b/quantum_launcher/src/state/menu.rs @@ -6,6 +6,7 @@ use std::{ use crate::{ config::SIDEBAR_WIDTH, message_handler::get_locally_installed_mods, state::NotesMessage, }; +use ezshortcut::Shortcut; use frostmark::MarkState; use iced::{ widget::{self, scrollable::AbsoluteOffset}, @@ -632,10 +633,17 @@ pub enum State { LogUploadResult { url: String, }, + CreateShortcut(MenuShortcut), License(MenuLicense), } +pub struct MenuShortcut { + pub shortcut: Shortcut, + pub add_to_menu: bool, + pub add_to_desktop: bool, +} + pub struct MenuLicense { pub selected_tab: LicenseTab, pub content: iced::widget::text_editor::Content, diff --git a/quantum_launcher/src/state/message.rs b/quantum_launcher/src/state/message.rs index 073372849..49b1e9e09 100644 --- a/quantum_launcher/src/state/message.rs +++ b/quantum_launcher/src/state/message.rs @@ -352,6 +352,15 @@ pub enum GameLogMessage { Uploaded(Res), } +#[derive(Debug, Clone)] +pub enum ShortcutMessage { + Open, + ToggleAddToMenu(bool), + ToggleAddToDesktop(bool), + EditName(String), + EditDescription(String), +} + #[derive(Debug, Clone)] pub enum Message { Nothing, @@ -369,6 +378,7 @@ pub enum Message { Notes(NotesMessage), GameLog(GameLogMessage), Window(WindowMessage), + Shortcut(ShortcutMessage), ManageMods(ManageModsMessage), ManageJarMods(ManageJarModsMessage), diff --git a/quantum_launcher/src/tick.rs b/quantum_launcher/src/tick.rs index ef046f1fa..02af5d9d7 100644 --- a/quantum_launcher/src/tick.rs +++ b/quantum_launcher/src/tick.rs @@ -163,6 +163,7 @@ impl Launcher { | State::CurseforgeManualDownload(_) | State::LogUploadResult { .. } | State::InstallPaper(_) + | State::CreateShortcut(_) | State::ExportMods(_) => {} } diff --git a/quantum_launcher/src/update.rs b/quantum_launcher/src/update.rs index 33f04a130..5e4ee76a6 100644 --- a/quantum_launcher/src/update.rs +++ b/quantum_launcher/src/update.rs @@ -69,6 +69,7 @@ impl Launcher { Message::Window(msg) => return self.update_window_msg(msg), Message::Notes(msg) => return self.update_notes(msg), Message::GameLog(msg) => return self.update_game_log(msg), + Message::Shortcut(msg) => return self.update_shortcut(msg), Message::LaunchInstanceSelected(inst) => { self.selected_instance = Some(inst); diff --git a/quantum_launcher/src/view.rs b/quantum_launcher/src/view.rs index 272d7216b..a1d4704bb 100644 --- a/quantum_launcher/src/view.rs +++ b/quantum_launcher/src/view.rs @@ -151,6 +151,7 @@ impl Launcher { State::LogUploadResult { url } => { view_log_upload_result(url, self.instance().is_server()) } + State::CreateShortcut(menu) => menu.view(), State::LoginAlternate(menu) => menu.view(self.tick_timer), State::ExportInstance(menu) => menu.view(self.tick_timer), From 1874b0dd40c51aaacbd2beed8da1ebf4fcaa776a Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Tue, 10 Feb 2026 14:14:52 +0530 Subject: [PATCH 04/30] Docs: add feature to changelogs --- changelogs/unreleased.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/changelogs/unreleased.md b/changelogs/unreleased.md index dc188d65a..7eeed3b2b 100644 --- a/changelogs/unreleased.md +++ b/changelogs/unreleased.md @@ -1,6 +1,15 @@ # unreleased changelog -- Added quick uninstall button to Mod Store +- Added a Quick Uninstall button to the Mod Store + +# Shortcuts + +- Launch instances with a single click; no launcher required! +- Create one-click shortcuts for: + - Desktop + - Start Menu / Applications Menu (Windows/Linux) + - Applications (macOS) + - Other custom locations # Technical From 3a1671f51d4161de1ce018c7501370b9bf1aa2c0 Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Tue, 10 Feb 2026 14:15:00 +0530 Subject: [PATCH 05/30] Docs: clean up changelogs with better wording --- changelogs/unreleased.md | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/changelogs/unreleased.md b/changelogs/unreleased.md index 7eeed3b2b..5ea43a337 100644 --- a/changelogs/unreleased.md +++ b/changelogs/unreleased.md @@ -13,30 +13,26 @@ # Technical -- Usernames in paths are now censored in logs - - eg: `C:\Users\YOUR_NAME` or `/home/YOUR_NAME` -> - `C:\Users\[REDACTED]` or `/home/[REDACTED]` - - Use `--no-redact-info` CLI flag to temporarily disable this +- Usernames are now redacted in log paths + - eg: `C:\Users\YOUR_NAME` -> `C:\Users\[REDACTED]` + - Disable temporarily with `--no-redact-info` flag ## Java -- In addition to custom Java paths, you can now choose - different launcher-provided Java versions as well -- The java installer has been improved with better platform support - - For example, you can now run Minecraft 1.20.5 to 1.21.11 - on many 32-bit systems -- For platforms without Mojang-provided Java, - we now use Azul Zulu instead of Amazon Corretto +- Select launcher-provided Java versions in addition to custom paths +- Improved Java installer with expanded platform support + - Minecraft 1.20.5–1.21.11 now runs on many 32-bit systems +- Platforms without Mojang Java now use **Azul Zulu** instead of Amazon Corretto # Fixes -- Fixed many concurrent downloading bugs with CurseForge -- Fixed account login being broken for new users -- Fixed versions after 1.21.11 (eg: snapshots) not launching on Linux ARM -- Fixed Java being frequently redownloaded on some ARM systems +- Fixed many CurseForge concurrent downloading issues +- Fixed account login persistence for new users +- Fixed post-1.21.11 versions (eg: snapshots) not launching on Linux ARM +- Fixed unnecessary Java redownloads on some ARM systems ## Logging -- Overhauled log viewer code, now with text selection, - better scrolling, and fewer bugs -- Fixed game crash reports not showing in logs + +- Overhauled log viewer: text selection, better scrolling, fewer bugs +- Fixed missing crash reports in logs - Added warning when running in macOS VM From 47ce4c98a4dc9f4ea211d75a9cc8982c06b4de1a Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Tue, 10 Feb 2026 17:36:11 +0530 Subject: [PATCH 06/30] Dev: tweaks to platform handling/api, add EXTENSION and get_filename --- crates/ezshortcut/src/async_impl/macos.rs | 1 - crates/ezshortcut/src/async_impl/mod.rs | 17 -------------- crates/ezshortcut/src/async_impl/windows.rs | 1 - crates/ezshortcut/src/lib.rs | 16 +++++++++----- crates/ezshortcut/src/os/macos.rs | 13 +++++++++++ .../ezshortcut/src/{sync_impl => os}/mod.rs | 4 ++++ .../ezshortcut/src/{async_impl => os}/stub.rs | 6 ++++- .../ezshortcut/src/{async_impl => os}/unix.rs | 22 +++++++++++-------- crates/ezshortcut/src/os/windows.rs | 13 +++++++++++ crates/ezshortcut/src/sync_impl/macos.rs | 5 ----- crates/ezshortcut/src/sync_impl/stub.rs | 5 ----- crates/ezshortcut/src/sync_impl/unix.rs | 9 -------- crates/ezshortcut/src/sync_impl/windows.rs | 5 ----- 13 files changed, 59 insertions(+), 58 deletions(-) delete mode 100644 crates/ezshortcut/src/async_impl/macos.rs delete mode 100644 crates/ezshortcut/src/async_impl/mod.rs delete mode 100644 crates/ezshortcut/src/async_impl/windows.rs create mode 100644 crates/ezshortcut/src/os/macos.rs rename crates/ezshortcut/src/{sync_impl => os}/mod.rs (67%) rename crates/ezshortcut/src/{async_impl => os}/stub.rs (73%) rename crates/ezshortcut/src/{async_impl => os}/unix.rs (76%) create mode 100644 crates/ezshortcut/src/os/windows.rs delete mode 100644 crates/ezshortcut/src/sync_impl/macos.rs delete mode 100644 crates/ezshortcut/src/sync_impl/stub.rs delete mode 100644 crates/ezshortcut/src/sync_impl/unix.rs delete mode 100644 crates/ezshortcut/src/sync_impl/windows.rs diff --git a/crates/ezshortcut/src/async_impl/macos.rs b/crates/ezshortcut/src/async_impl/macos.rs deleted file mode 100644 index 8b1378917..000000000 --- a/crates/ezshortcut/src/async_impl/macos.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/ezshortcut/src/async_impl/mod.rs b/crates/ezshortcut/src/async_impl/mod.rs deleted file mode 100644 index 66befdf77..000000000 --- a/crates/ezshortcut/src/async_impl/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -use cfg_if::cfg_if; - -cfg_if!( - if #[cfg(target_os = "windows")] { - mod windows; - pub use windows::*; - } else if #[cfg(target_os = "macos")] { - mod macos; - pub use macos::*; - } else if #[cfg(target_family = "unix")] { - mod unix; - pub use unix::*; - } else { - mod stub; - pub use stub::*; - } -); diff --git a/crates/ezshortcut/src/async_impl/windows.rs b/crates/ezshortcut/src/async_impl/windows.rs deleted file mode 100644 index 8b1378917..000000000 --- a/crates/ezshortcut/src/async_impl/windows.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/ezshortcut/src/lib.rs b/crates/ezshortcut/src/lib.rs index ef3e32987..fd232860e 100644 --- a/crates/ezshortcut/src/lib.rs +++ b/crates/ezshortcut/src/lib.rs @@ -1,12 +1,12 @@ use std::path::Path; -mod async_impl; -mod sync_impl; - -pub use sync_impl::get_menu_path; +mod os; +pub use os::{get_menu_path, EXTENSION}; +#[derive(Debug, Clone)] pub struct Shortcut { pub name: String, + /// Linux/BSD/Unix only, leave blank for none pub description: String, pub exec: String, pub icon: Option, @@ -14,7 +14,13 @@ pub struct Shortcut { impl Shortcut { pub async fn generate(&self, path: &Path) -> std::io::Result<()> { - async_impl::create(self, path).await + os::create(self, path).await + } + + pub fn get_filename(&self) -> String { + let mut filtered_name = make_filename_safe(&self.name, !cfg!(target_os = "windows")); + filtered_name.push_str(EXTENSION); + filtered_name } } diff --git a/crates/ezshortcut/src/os/macos.rs b/crates/ezshortcut/src/os/macos.rs new file mode 100644 index 000000000..d4f1f42bb --- /dev/null +++ b/crates/ezshortcut/src/os/macos.rs @@ -0,0 +1,13 @@ +use crate::Shortcut; +use std::path::{Path, PathBuf}; + +pub fn get_menu_path() -> Option { + dirs::home_dir().map(|h| h.join("Applications")) +} + +pub async fn create(shortcut: &Shortcut, path: impl AsRef) -> std::io::Result<()> { + Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "Shortcuts aren't supported on this platform", + )) +} diff --git a/crates/ezshortcut/src/sync_impl/mod.rs b/crates/ezshortcut/src/os/mod.rs similarity index 67% rename from crates/ezshortcut/src/sync_impl/mod.rs rename to crates/ezshortcut/src/os/mod.rs index 66befdf77..bf7e23dc1 100644 --- a/crates/ezshortcut/src/sync_impl/mod.rs +++ b/crates/ezshortcut/src/os/mod.rs @@ -4,14 +4,18 @@ cfg_if!( if #[cfg(target_os = "windows")] { mod windows; pub use windows::*; + pub const EXTENSION: &str = ".lnk"; } else if #[cfg(target_os = "macos")] { mod macos; pub use macos::*; + pub const EXTENSION: &str = ".app"; } else if #[cfg(target_family = "unix")] { mod unix; pub use unix::*; + pub const EXTENSION: &str = ".desktop"; } else { mod stub; pub use stub::*; + pub const EXTENSION: &str = ""; } ); diff --git a/crates/ezshortcut/src/async_impl/stub.rs b/crates/ezshortcut/src/os/stub.rs similarity index 73% rename from crates/ezshortcut/src/async_impl/stub.rs rename to crates/ezshortcut/src/os/stub.rs index 3973f36a3..aa4aecfb5 100644 --- a/crates/ezshortcut/src/async_impl/stub.rs +++ b/crates/ezshortcut/src/os/stub.rs @@ -1,5 +1,9 @@ use crate::Shortcut; -use std::path::Path; +use std::path::{Path, PathBuf}; + +pub fn get_menu_path() -> Option { + None +} pub async fn create(shortcut: &Shortcut, path: impl AsRef) -> std::io::Result<()> { Err(std::io::Error::new( diff --git a/crates/ezshortcut/src/async_impl/unix.rs b/crates/ezshortcut/src/os/unix.rs similarity index 76% rename from crates/ezshortcut/src/async_impl/unix.rs rename to crates/ezshortcut/src/os/unix.rs index cf1a27b33..9d31b27b7 100644 --- a/crates/ezshortcut/src/async_impl/unix.rs +++ b/crates/ezshortcut/src/os/unix.rs @@ -1,9 +1,20 @@ -use std::{os::unix::fs::PermissionsExt, path::Path}; +use std::{ + os::unix::fs::PermissionsExt, + path::{Path, PathBuf}, +}; use tokio::fs; use crate::{make_filename_safe, Shortcut}; +pub fn get_menu_path() -> Option { + if let Ok(dir) = std::env::var("XDG_DATA_HOME") { + Some(PathBuf::from(dir).join("applications")) + } else { + dirs::home_dir().map(|h| h.join(".local/share/applications")) + } +} + pub async fn create(shortcut: &Shortcut, path: impl AsRef) -> std::io::Result<()> { let path = path.as_ref(); create_inner(shortcut, path).await?; @@ -38,14 +49,7 @@ Categories=Game;", match fs::metadata(path).await { Ok(n) => { if n.is_dir() { - write_file( - &path.join(format!( - "{}.desktop", - make_filename_safe(&shortcut.name, true) - )), - content, - ) - .await?; + write_file(&path.join(shortcut.get_filename()), content).await?; } } _ => write_file(path, content).await?, diff --git a/crates/ezshortcut/src/os/windows.rs b/crates/ezshortcut/src/os/windows.rs new file mode 100644 index 000000000..581ec96cc --- /dev/null +++ b/crates/ezshortcut/src/os/windows.rs @@ -0,0 +1,13 @@ +use crate::Shortcut; +use std::path::{Path, PathBuf}; + +pub fn get_menu_path() -> Option { + dirs::data_dir().map(|d| d.join("Microsoft/Windows/Start Menu/Programs")) +} + +pub async fn create(shortcut: &Shortcut, path: impl AsRef) -> std::io::Result<()> { + Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "Shortcuts aren't supported on this platform", + )) +} diff --git a/crates/ezshortcut/src/sync_impl/macos.rs b/crates/ezshortcut/src/sync_impl/macos.rs deleted file mode 100644 index 60cc1c0e0..000000000 --- a/crates/ezshortcut/src/sync_impl/macos.rs +++ /dev/null @@ -1,5 +0,0 @@ -use std::path::PathBuf; - -pub fn get_menu_path() -> Option { - dirs::home_dir().map(|h| h.join("Applications")) -} diff --git a/crates/ezshortcut/src/sync_impl/stub.rs b/crates/ezshortcut/src/sync_impl/stub.rs deleted file mode 100644 index 174bb035b..000000000 --- a/crates/ezshortcut/src/sync_impl/stub.rs +++ /dev/null @@ -1,5 +0,0 @@ -use std::path::PathBuf; - -pub fn get_menu_path() -> Option { - None -} diff --git a/crates/ezshortcut/src/sync_impl/unix.rs b/crates/ezshortcut/src/sync_impl/unix.rs deleted file mode 100644 index e742f1137..000000000 --- a/crates/ezshortcut/src/sync_impl/unix.rs +++ /dev/null @@ -1,9 +0,0 @@ -use std::path::PathBuf; - -pub fn get_menu_path() -> Option { - if let Ok(dir) = std::env::var("XDG_DATA_HOME") { - Some(PathBuf::from(dir).join("applications")) - } else { - dirs::home_dir().map(|h| h.join(".local/share/applications")) - } -} diff --git a/crates/ezshortcut/src/sync_impl/windows.rs b/crates/ezshortcut/src/sync_impl/windows.rs deleted file mode 100644 index ac6c4bbf6..000000000 --- a/crates/ezshortcut/src/sync_impl/windows.rs +++ /dev/null @@ -1,5 +0,0 @@ -use std::path::PathBuf; - -pub fn get_menu_path() -> Option { - dirs::data_dir().map(|d| d.join("Microsoft/Windows/Start Menu/Programs")) -} From 06dbdce9aa534655babba8d90f0b1f15b26d3dd9 Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Tue, 10 Feb 2026 17:39:04 +0530 Subject: [PATCH 07/30] Dev: Add `--show-progress` CLI arg for desktop notification when launching headlessly --- Cargo.lock | 90 +++++++++++++++++++++++++++++ changelogs/unreleased.md | 3 + quantum_launcher/Cargo.toml | 1 + quantum_launcher/src/cli/command.rs | 13 ++++- quantum_launcher/src/cli/mod.rs | 28 ++++++++- 5 files changed, 132 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2595c41bd..c949b6e44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1144,6 +1144,15 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + [[package]] name = "derive_arbitrary" version = "1.4.2" @@ -2866,6 +2875,18 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mac-notification-sys" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fd3f75411f4725061682ed91f131946e912859d0044d39c4ec0aac818d7621" +dependencies = [ + "cc", + "objc2 0.6.2", + "objc2-foundation 0.3.1", + "time", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -3070,6 +3091,20 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "notify-rust" +version = "4.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21af20a1b50be5ac5861f74af1a863da53a11c38684d9818d82f1c42f7fdc6c2" +dependencies = [ + "futures-lite", + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + [[package]] name = "notify-types" version = "2.0.0" @@ -3118,6 +3153,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.46" @@ -3356,6 +3397,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" dependencies = [ "bitflags 2.9.2", + "block2 0.6.1", + "libc", "objc2 0.6.2", "objc2-core-foundation", ] @@ -3886,6 +3929,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -4080,6 +4129,7 @@ dependencies = [ "frostmark", "iced", "notify", + "notify-rust", "open", "owo-colors", "paste", @@ -5138,6 +5188,18 @@ dependencies = [ "xattr", ] +[[package]] +name = "tauri-winrt-notification" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" +dependencies = [ + "quick-xml", + "thiserror 2.0.14", + "windows 0.61.3", + "windows-version", +] + [[package]] name = "tempfile" version = "3.20.0" @@ -5250,6 +5312,25 @@ dependencies = [ "weezl", ] +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -6449,6 +6530,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" diff --git a/changelogs/unreleased.md b/changelogs/unreleased.md index e0e676b06..83bda4bc4 100644 --- a/changelogs/unreleased.md +++ b/changelogs/unreleased.md @@ -19,6 +19,9 @@ - Usernames are now redacted in log paths - eg: `C:\Users\YOUR_NAME` -> `C:\Users\[REDACTED]` - Disable temporarily with `--no-redact-info` flag +- When launching headlessly (`quantum_launcher launch `), + you can now use `--show-progress` to get desktop notifications on account login progress or errors + - Especially useful for shortcuts/scripts ## Java diff --git a/quantum_launcher/Cargo.toml b/quantum_launcher/Cargo.toml index 583a53b05..dc51803c3 100644 --- a/quantum_launcher/Cargo.toml +++ b/quantum_launcher/Cargo.toml @@ -79,6 +79,7 @@ terminal_size = "0.4" notify = "8" open = "5.3.2" dark-light = "2" +notify-rust = { version = "4", default-features = false, features = ["z-with-tokio"] } # iced::widget::markdown doesn't have inline # HTML support, so I made my own renderer. diff --git a/quantum_launcher/src/cli/command.rs b/quantum_launcher/src/cli/command.rs index 38d86a192..e8fbfb30f 100644 --- a/quantum_launcher/src/cli/command.rs +++ b/quantum_launcher/src/cli/command.rs @@ -202,8 +202,9 @@ pub async fn launch_instance( username: String, use_account: bool, servers: bool, + show_progress: bool, ) -> Result<(), Box> { - let account = refresh_account(&username, use_account).await?; + let account = refresh_account(&username, use_account, show_progress).await?; let child = if servers { // TODO: stdin input @@ -242,6 +243,7 @@ pub async fn launch_instance( async fn refresh_account( username: &String, use_account: bool, + show_progress: bool, ) -> Result, Box> { Ok(if use_account { let config = LauncherConfig::load_s()?; @@ -256,6 +258,15 @@ async fn refresh_account( exit(1); }; + if show_progress { + tokio::task::spawn_blocking(|| { + notify_rust::Notification::new() + .summary("Launching game") + .body("Refreshing account...") + .show() + }); + }; + match account.account_type.as_deref() { // Hook: Account types Some(kind @ ("ElyBy" | "LittleSkin")) => { diff --git a/quantum_launcher/src/cli/mod.rs b/quantum_launcher/src/cli/mod.rs index 80d8550ff..4bc0865e1 100644 --- a/quantum_launcher/src/cli/mod.rs +++ b/quantum_launcher/src/cli/mod.rs @@ -54,9 +54,14 @@ enum QSubCommand { instance_name: String, #[arg(help = "Username to play with")] username: String, + + // Used by shortcuts, do not break #[arg(short, long, short_alias = 'a')] #[arg(help = "Whether to use a logged in account of the given username (if any)")] use_account: bool, + // Used by shortcuts + #[arg(long)] + show_progress: bool, }, #[command(aliases = ["list", "list-instances"], short_flag = 'l')] #[command(about = "Lists installed instances")] @@ -214,14 +219,26 @@ pub fn start_cli(is_dir_err: bool) { instance_name, username, use_account, + show_progress, } => { - quit(runtime.block_on(command::launch_instance( + let res = runtime.block_on(command::launch_instance( instance_name, username, use_account, cli.server, - ))); + show_progress, + )); + std::process::exit(if let Err(err) = res { + err!("{err}"); + if show_progress { + show_notification(err.to_string()); + } + 1 + } else { + 0 + }); } + QSubCommand::ListAvailableVersions => { command::list_available_versions(); std::process::exit(0); @@ -242,6 +259,13 @@ pub fn start_cli(is_dir_err: bool) { } } +fn show_notification(err: String) { + _ = notify_rust::Notification::new() + .summary("Error launching game") + .body(&err) + .show(); +} + fn quit(res: Result<(), Box>) { std::process::exit(if let Err(err) = res { err!("{err}"); From 4fdbe83a804bc4a416f142d75685877050cf1f2a Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Tue, 10 Feb 2026 17:40:47 +0530 Subject: [PATCH 08/30] Dev: Shortcut progress... - Add UI button to export shortcut to file and generate Exec field - Add account selection to shortcuts - Split shortcut menu view into separate module - Make `self.account_selected` no longer an Option --- quantum_launcher/src/menu_renderer/launch.rs | 25 ++-- quantum_launcher/src/menu_renderer/mod.rs | 93 +----------- .../src/menu_renderer/shortcut.rs | 141 ++++++++++++++++++ quantum_launcher/src/message_handler/mod.rs | 10 +- .../src/message_update/accounts.rs | 29 ++-- .../src/message_update/shortcuts.rs | 81 +++++++++- quantum_launcher/src/state/menu.rs | 2 + quantum_launcher/src/state/message.rs | 7 + quantum_launcher/src/state/mod.rs | 8 +- quantum_launcher/src/view.rs | 2 +- 10 files changed, 260 insertions(+), 138 deletions(-) create mode 100644 quantum_launcher/src/menu_renderer/shortcut.rs diff --git a/quantum_launcher/src/menu_renderer/launch.rs b/quantum_launcher/src/menu_renderer/launch.rs index c8680ff9e..cf2b071a6 100644 --- a/quantum_launcher/src/menu_renderer/launch.rs +++ b/quantum_launcher/src/menu_renderer/launch.rs @@ -17,7 +17,7 @@ use crate::{ menu_renderer::DISCORD, state::{ AccountMessage, CreateInstanceMessage, InstanceLog, LaunchTab, Launcher, - LauncherSettingsMessage, ManageModsMessage, MenuLaunch, Message, State, NEW_ACCOUNT_NAME, + LauncherSettingsMessage, ManageModsMessage, MenuLaunch, Message, State, OFFLINE_ACCOUNT_NAME, }, stylesheet::{color::Color, styles::LauncherTheme, widgets::StyleButton}, @@ -412,13 +412,13 @@ impl Launcher { let something_is_happening = self.java_recv.is_some() || menu.login_progress.is_some(); let dropdown: Element = if something_is_happening { - widget::text_input("", self.accounts_selected.as_deref().unwrap_or_default()) + widget::text_input("", &self.account_selected) .width(Length::Fill) .into() } else { widget::pick_list( - self.accounts_dropdown.clone(), - self.accounts_selected.clone(), + self.accounts_dropdown.as_slice(), + Some(&self.account_selected), |n| Message::Account(AccountMessage::Selected(n)), ) .width(Length::Fill) @@ -427,7 +427,7 @@ impl Launcher { widget::column![ widget::row![widget::text(" Accounts:").size(14), horizontal_space()].push_maybe( - self.is_account_selected().then_some( + (self.account_selected != OFFLINE_ACCOUNT_NAME).then_some( widget::button(widget::text("Logout").size(11)) .padding(3) .on_press(Message::Account(AccountMessage::LogoutCheck)) @@ -438,7 +438,7 @@ impl Launcher { dropdown ] .push_maybe( - (self.accounts_selected.as_deref() == Some(OFFLINE_ACCOUNT_NAME)).then_some( + (self.account_selected == OFFLINE_ACCOUNT_NAME).then_some( widget::text_input("Enter username...", &self.config.username) .on_input(Message::LaunchUsernameSet) .width(Length::Fill), @@ -449,20 +449,13 @@ impl Launcher { .into() } - pub fn is_account_selected(&self) -> bool { - !(self.accounts_selected.is_none() - || self.accounts_selected.as_deref() == Some(NEW_ACCOUNT_NAME) - || self.accounts_selected.as_deref() == Some(OFFLINE_ACCOUNT_NAME)) - } - fn get_client_play_button(&'_ self) -> Element<'_> { let play_button = button_with_icon(icons::play(), "Play", 16).width(98); + let is_offline = self.account_selected == OFFLINE_ACCOUNT_NAME; - let is_account_selected = self.is_account_selected(); - - if self.config.username.is_empty() && !is_account_selected { + if self.config.username.is_empty() && is_offline { tooltip(play_button, "Username is empty!", Position::Bottom).into() - } else if self.config.username.contains(' ') && !is_account_selected { + } else if self.config.username.contains(' ') && is_offline { tooltip(play_button, "Username contains spaces!", Position::Bottom).into() } else if let Some(selected_instance) = &self.selected_instance { if self.processes.contains_key(selected_instance) { diff --git a/quantum_launcher/src/menu_renderer/mod.rs b/quantum_launcher/src/menu_renderer/mod.rs index 7d1955a81..f9e24ba6a 100644 --- a/quantum_launcher/src/menu_renderer/mod.rs +++ b/quantum_launcher/src/menu_renderer/mod.rs @@ -1,4 +1,3 @@ -use cfg_if::cfg_if; use iced::{ widget::{self, column, row, tooltip::Position}, Alignment, Length, @@ -6,7 +5,6 @@ use iced::{ use ql_core::{Progress, WEBSITE}; use ql_instances::auth::AccountType; -use crate::state::{MenuShortcut, ShortcutMessage}; use crate::stylesheet::styles::{LauncherThemeLightness, BORDER_RADIUS, BORDER_WIDTH}; use crate::{ config::LauncherConfig, @@ -26,6 +24,7 @@ mod login; mod mods; mod onboarding; mod settings; +mod shortcut; pub use onboarding::changelog; @@ -407,96 +406,6 @@ impl MenuLicense { } } -impl MenuShortcut { - pub fn view(&self) -> Element<'_> { - cfg_if!(if #[cfg(target_os = "windows")] { - const MENU_NAME: &str = "the Start Menu"; - const IS_UNIX: bool = false; - } else if #[cfg(target_os = "macos")] { - const MENU_NAME: &str = "Applications"; - const IS_UNIX: bool = false; - } else { - const MENU_NAME: &str = "the Applications Menu"; - const IS_UNIX: bool = true; - }); - - row![ - widget::scrollable( - column![ - row![ - back_button().on_press(Message::MScreenOpen { - message: None, - clear_selection: false, - is_server: None - }), - widget::text("Create Launch Shortcut").size(20), - ] - .align_y(Alignment::Center) - .spacing(16), - widget::text!("Launch this instance directly from your Desktop or {MENU_NAME} without opening the launcher") - .size(14) - .style(tsubtitle), - column![row![ - "Name:", - widget::text_input("(Required)", &self.shortcut.name) - .size(14) - .on_input(|n| Message::Shortcut(ShortcutMessage::EditName(n))) - ].spacing(10).align_y(Alignment::Center)] - .spacing(5) - .push_maybe( - IS_UNIX.then_some( - row![ - "Description:", - widget::text_input("Leave blank for none", &self.shortcut.description) - .size(14) - .on_input(|n| Message::Shortcut(ShortcutMessage::EditDescription(n))) - ].align_y(Alignment::Center).spacing(10) - ) - ), - column![ - widget::checkbox(format!("Add to {MENU_NAME}"), self.add_to_menu) - .on_toggle(|t| Message::Shortcut(ShortcutMessage::ToggleAddToMenu(t))) - .size(12) - .text_size(12), - widget::checkbox(format!("Add to Desktop"), self.add_to_desktop) - .on_toggle(|t| Message::Shortcut(ShortcutMessage::ToggleAddToDesktop(t))) - .size(12) - .text_size(12), - button_with_icon(icons::checkmark_s(14), "Create Shortcut", 14).on_press(Message::Nothing), - ] - .spacing(5), - column![ - widget::text("Or save a shortcut file to use anywhere") - .size(14) - .style(tsubtitle), - button_with_icon(icons::floppydisk_s(14), "Export Shortcut File...", 14).on_press(Message::Nothing), - ] - .spacing(5) - ] - .width(Length::Fill) - .padding(16) - .spacing(16) - ) - .style(|t: &LauncherTheme, s| t.style_scrollable_flat_dark(s)), - widget::scrollable( - column![ - widget::text("Existing Shortcuts").size(20), - widget::text( - "TODO: Store and show shortcut info here, and have options to delete them" - ) - .size(12) - ] - .padding(16) - .spacing(10) - ) - .height(Length::Fill) - .width(200) - .style(|t: &LauncherTheme, s| t.style_scrollable_flat_extra_dark(s)) - ] - .into() - } -} - pub fn view_account_login<'a>() -> Element<'a> { column![ back_button().on_press(back_to_launch_screen(None, None)), diff --git a/quantum_launcher/src/menu_renderer/shortcut.rs b/quantum_launcher/src/menu_renderer/shortcut.rs new file mode 100644 index 000000000..baeb5ed9f --- /dev/null +++ b/quantum_launcher/src/menu_renderer/shortcut.rs @@ -0,0 +1,141 @@ +use crate::{ + icons, + menu_renderer::{back_button, button_with_icon, tsubtitle, Element}, + state::{MenuShortcut, Message, ShortcutMessage, OFFLINE_ACCOUNT_NAME}, + stylesheet::styles::LauncherTheme, +}; +use cfg_if::cfg_if; +use iced::{ + widget::{self, column, row}, + Alignment, Length, +}; + +cfg_if!(if #[cfg(target_os = "windows")] { + const MENU_NAME: &str = "the Start Menu"; + const IS_UNIX: bool = false; +} else if #[cfg(target_os = "macos")] { + const MENU_NAME: &str = "Applications"; + const IS_UNIX: bool = false; +} else { + const MENU_NAME: &str = "the Applications Menu"; + const IS_UNIX: bool = true; +}); + +impl MenuShortcut { + pub fn view<'a>(&'a self, accounts: &'a [String]) -> Element<'a> { + row![ + widget::scrollable( + column![ + row![ + back_button().on_press(Message::MScreenOpen { + message: None, + clear_selection: false, + is_server: None + }), + widget::text("Create Launch Shortcut").size(20), + ] + .align_y(Alignment::Center) + .spacing(16), + column![ + widget::text!("Launch this instance directly from your Desktop or {MENU_NAME} without opening the launcher") + .size(14) + .style(tsubtitle), + widget::text("Note: You can manually pin this to Taskbar/Dock/Panel later").size(12).style(tsubtitle) + ].spacing(5), + + // Shortcut information (name, account, etc.) + self.get_info_fields(accounts), + + row![ + button_with_icon(icons::checkmark_s(14), "Create Shortcut", 14).on_press(Message::Nothing), + column![ + widget::checkbox(format!("Add to {MENU_NAME}"), self.add_to_menu) + .on_toggle(|t| Message::Shortcut(ShortcutMessage::ToggleAddToMenu(t))) + .size(12) + .text_size(12), + widget::checkbox(format!("Add to Desktop"), self.add_to_desktop) + .on_toggle(|t| Message::Shortcut(ShortcutMessage::ToggleAddToDesktop(t))) + .size(12) + .text_size(12), + ] + ] + .spacing(10), + column![ + widget::text("Or save a shortcut file to use anywhere") + .size(14) + .style(tsubtitle), + widget::text("(May not work everywhere)").size(12).style(tsubtitle), + widget::Space::with_height(5), + button_with_icon(icons::floppydisk_s(14), "Export Shortcut File...", 14).on_press(Message::Shortcut(ShortcutMessage::SaveCustom)), + ] + ] + .width(Length::Fill) + .padding(16) + .spacing(12) + ) + .style(|t: &LauncherTheme, s| t.style_scrollable_flat_dark(s)), + widget::scrollable( + column![ + widget::text("Existing Shortcuts").size(20), + widget::text( + "TODO: Store and show shortcut info here, and have options to delete them" + ) + .size(12) + ] + .padding(16) + .spacing(10) + ) + .height(Length::Fill) + .width(200) + .style(|t: &LauncherTheme, s| t.style_scrollable_flat_extra_dark(s)) + ] + .into() + } + + fn get_info_fields<'a>( + &'a self, + accounts: &'a [String], + ) -> widget::Column<'a, Message, LauncherTheme> { + fn ifield<'a>( + name: &'a str, + elem: impl Into>, + ) -> widget::Row<'a, Message, LauncherTheme> { + row![widget::text(name).size(14).width(100), elem.into()] + .spacing(10) + .align_y(Alignment::Center) + } + + column![ifield( + " Name:", + widget::text_input("(Required)", &self.shortcut.name) + .size(14) + .on_input(|n| Message::Shortcut(ShortcutMessage::EditName(n))) + )] + .push_maybe(IS_UNIX.then(|| { + ifield( + " Description:", + widget::text_input("Leave blank for none", &self.shortcut.description) + .size(14) + .on_input(|n| Message::Shortcut(ShortcutMessage::EditDescription(n))), + ) + })) + .push(ifield( + " Account:", + row![widget::pick_list(accounts, Some(&self.account), |n| { + Message::Shortcut(ShortcutMessage::AccountSelected(n)) + }) + .text_size(14) + .width(Length::Fill)] + .push_maybe( + (self.account == OFFLINE_ACCOUNT_NAME).then_some( + widget::text_input("Enter username...", &self.account_offline) + .size(14) + .width(Length::Fill) + .on_input(|n| Message::Shortcut(ShortcutMessage::AccountOffline(n))), + ), + ) + .spacing(5), + )) + .spacing(5) + } +} diff --git a/quantum_launcher/src/message_handler/mod.rs b/quantum_launcher/src/message_handler/mod.rs index 4f2158511..cf00953ce 100644 --- a/quantum_launcher/src/message_handler/mod.rs +++ b/quantum_launcher/src/message_handler/mod.rs @@ -638,12 +638,10 @@ impl Launcher { match selected_instance { InstanceSelection::Instance(_) => { - if let Some(account) = &self.accounts_selected { - if account == OFFLINE_ACCOUNT_NAME - && (self.config.username.is_empty() || self.config.username.contains(' ')) - { - return Task::none(); - } + if self.account_selected == OFFLINE_ACCOUNT_NAME + && (self.config.username.is_empty() || self.config.username.contains(' ')) + { + return Task::none(); } self.is_launching_game = true; diff --git a/quantum_launcher/src/message_update/accounts.rs b/quantum_launcher/src/message_update/accounts.rs index 6a33ebe71..ef7773f56 100644 --- a/quantum_launcher/src/message_update/accounts.rs +++ b/quantum_launcher/src/message_update/accounts.rs @@ -33,9 +33,7 @@ impl Launcher { | AccountMessage::RefreshComplete(Err(err)) => { self.set_error(err); } - AccountMessage::Selected(account) => { - return self.account_selected(account); - } + AccountMessage::Selected(account) => self.account_selected(account), AccountMessage::Response1 { r: Ok(code), is_from_welcome_screen, @@ -49,9 +47,8 @@ impl Launcher { return self.account_response_3(data); } AccountMessage::LogoutCheck => { - let username = self.accounts_selected.as_ref().unwrap(); self.state = State::ConfirmAction { - msg1: format!("log out of your account: {username}"), + msg1: format!("log out of your account: {}", self.account_selected), msg2: "You can always log in later".to_owned(), yes: Message::Account(AccountMessage::LogoutConfirm), no: Message::MScreenOpen { @@ -104,7 +101,7 @@ impl Launcher { } AccountMessage::LogoutConfirm => { self.autosave.remove(&AutoSaveKind::LauncherConfig); - let username = self.accounts_selected.clone().unwrap(); + let username = self.account_selected.clone(); let account_type = self .accounts .get(&username) @@ -130,7 +127,7 @@ impl Launcher { .first() .cloned() .unwrap_or_else(|| OFFLINE_ACCOUNT_NAME.to_owned()); - self.accounts_selected = Some(selected_account); + self.account_selected = selected_account; return self.go_to_launch_screen(Option::::None); } @@ -257,16 +254,15 @@ impl Launcher { Task::none() } - fn account_selected(&mut self, account: String) -> Task { + fn account_selected(&mut self, account: String) { if account == NEW_ACCOUNT_NAME { self.state = State::AccountLogin; } else { if account != OFFLINE_ACCOUNT_NAME { self.config.account_selected = Some(account.clone()); } - self.accounts_selected = Some(account); + self.account_selected = account; } - Task::none() } pub fn account_refresh(&mut self, account: &AccountData) -> Task { @@ -320,7 +316,7 @@ impl Launcher { }, ); - self.accounts_selected = Some(username.clone()); + self.account_selected = username.clone(); self.accounts.insert(username.clone(), data); self.go_to_launch_screen::(None) @@ -355,14 +351,11 @@ impl Launcher { } pub fn get_selected_account_data(&self) -> Option { - if let Some(account) = &self.accounts_selected { - if account == NEW_ACCOUNT_NAME || account == OFFLINE_ACCOUNT_NAME { - None - } else { - self.accounts.get(account).cloned() - } - } else { + let account = &self.account_selected; + if account == NEW_ACCOUNT_NAME || account == OFFLINE_ACCOUNT_NAME { None + } else { + self.accounts.get(account).cloned() } } } diff --git a/quantum_launcher/src/message_update/shortcuts.rs b/quantum_launcher/src/message_update/shortcuts.rs index 94d003190..85b595d53 100644 --- a/quantum_launcher/src/message_update/shortcuts.rs +++ b/quantum_launcher/src/message_update/shortcuts.rs @@ -1,7 +1,10 @@ use ezshortcut::Shortcut; use iced::Task; +use ql_core::{info, IntoStringError}; -use crate::state::{Launcher, MenuShortcut, Message, ShortcutMessage, State}; +use crate::state::{ + Launcher, MenuShortcut, Message, ShortcutMessage, State, NEW_ACCOUNT_NAME, OFFLINE_ACCOUNT_NAME, +}; macro_rules! iflet { ($m:ident, $s:expr, $b:block) => {{ @@ -24,6 +27,8 @@ impl Launcher { }, add_to_menu: true, add_to_desktop: false, + account: self.account_selected.clone(), + account_offline: self.config.username.clone(), }) } ShortcutMessage::ToggleAddToMenu(t) => iflet!(menu, self.state, { @@ -42,6 +47,80 @@ impl Launcher { ShortcutMessage::EditDescription(desc) => iflet!(menu, self.state, { menu.shortcut.description = desc; }), + ShortcutMessage::AccountSelected(acc) => iflet!(menu, self.state, { + if acc == NEW_ACCOUNT_NAME { + self.state = State::AccountLogin; + } else { + menu.account = acc; + } + }), + ShortcutMessage::AccountOffline(acc) => iflet!(menu, self.state, { + menu.account_offline = acc; + }), + + ShortcutMessage::SaveCustom => iflet!(menu, self.state, { + return Task::perform( + rfd::AsyncFileDialog::new() + .add_filter("Shortcut", &[ezshortcut::EXTENSION]) + .set_file_name(menu.shortcut.get_filename()) + .set_title("Save shortcut to...") + .save_file(), + |f| { + if let Some(f) = f { + Message::Shortcut(ShortcutMessage::SaveCustomPicked( + f.path().to_owned(), + )) + } else { + Message::Nothing + } + }, + ); + }), + ShortcutMessage::SaveCustomPicked(f) => iflet!(menu, self.state, { + let mut shortcut = menu.shortcut.clone(); + let instance = self.selected_instance.as_ref().unwrap(); + + let exec_path = match std::env::current_exe() { + Ok(n) => n, + Err(err) => { + self.set_error(format!("while getting path to current exe:\n{err}")); + return Task::none(); + } + }; + let launch = format!( + "{exe} {server}launch {name} {username}{login} --show-progress", + exe = exec_path.to_string_lossy(), + server = if instance.is_server() { + "--enable-server-manager -s " + } else { + "" + }, + name = serde_json::to_string(&instance.get_name()) + .unwrap_or_else(|_| instance.get_name().to_owned()), + username = if menu.account == OFFLINE_ACCOUNT_NAME { + &menu.account_offline + } else { + &menu.account + }, + login = if menu.account == OFFLINE_ACCOUNT_NAME { + "" + } else { + " -u" + } + ); + shortcut.exec = launch; + + return Task::perform(async move { shortcut.generate(&f).await }, |n| { + Message::Shortcut(ShortcutMessage::Done(n.strerr())) + }); + }), + ShortcutMessage::Done(r) => match r { + Ok(()) => { + info!("Created instance shortcut"); + // TODO: keep track of created shortcuts + } + Err(err) => self.set_error(err), + }, } Task::none() } diff --git a/quantum_launcher/src/state/menu.rs b/quantum_launcher/src/state/menu.rs index 1268a01e3..bae28f09d 100644 --- a/quantum_launcher/src/state/menu.rs +++ b/quantum_launcher/src/state/menu.rs @@ -643,6 +643,8 @@ pub struct MenuShortcut { pub shortcut: Shortcut, pub add_to_menu: bool, pub add_to_desktop: bool, + pub account: String, + pub account_offline: String, } pub struct MenuLicense { diff --git a/quantum_launcher/src/state/message.rs b/quantum_launcher/src/state/message.rs index fd531f658..48afc4c98 100644 --- a/quantum_launcher/src/state/message.rs +++ b/quantum_launcher/src/state/message.rs @@ -360,6 +360,13 @@ pub enum ShortcutMessage { ToggleAddToDesktop(bool), EditName(String), EditDescription(String), + + AccountSelected(String), + AccountOffline(String), + + SaveCustom, + SaveCustomPicked(PathBuf), + Done(Res), } #[derive(Debug, Clone)] diff --git a/quantum_launcher/src/state/mod.rs b/quantum_launcher/src/state/mod.rs index fa3953f61..dbfbc69df 100644 --- a/quantum_launcher/src/state/mod.rs +++ b/quantum_launcher/src/state/mod.rs @@ -63,7 +63,7 @@ pub struct Launcher { pub accounts: HashMap, pub accounts_dropdown: Vec, - pub accounts_selected: Option, + pub account_selected: String, pub client_list: Option>, pub server_list: Option>, @@ -159,7 +159,7 @@ impl Launcher { State::ChangeLog }; - let (accounts, accounts_dropdown, selected_account) = load_accounts(&mut config); + let (accounts, accounts_dropdown, account_selected) = load_accounts(&mut config); let persistent = config.c_persistent(); @@ -180,7 +180,7 @@ impl Launcher { mouse_pos: (0.0, 0.0), is_maximized: false, }, - accounts_selected: Some(selected_account), + account_selected, client_list: None, server_list: None, @@ -263,7 +263,7 @@ impl Launcher { }, autosave: HashSet::new(), accounts_dropdown: vec![OFFLINE_ACCOUNT_NAME.to_owned(), NEW_ACCOUNT_NAME.to_owned()], - accounts_selected: Some(OFFLINE_ACCOUNT_NAME.to_owned()), + account_selected: OFFLINE_ACCOUNT_NAME.to_owned(), modifiers_pressed: iced::keyboard::Modifiers::empty(), } } diff --git a/quantum_launcher/src/view.rs b/quantum_launcher/src/view.rs index a1d4704bb..0b43630c7 100644 --- a/quantum_launcher/src/view.rs +++ b/quantum_launcher/src/view.rs @@ -151,7 +151,7 @@ impl Launcher { State::LogUploadResult { url } => { view_log_upload_result(url, self.instance().is_server()) } - State::CreateShortcut(menu) => menu.view(), + State::CreateShortcut(menu) => menu.view(&self.accounts_dropdown), State::LoginAlternate(menu) => menu.view(self.tick_timer), State::ExportInstance(menu) => menu.view(self.tick_timer), From 389a0948e06807d6e5824feba355b4f448d16e37 Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Tue, 10 Feb 2026 17:41:58 +0530 Subject: [PATCH 09/30] Clean: lints/warnings --- crates/ezshortcut/src/os/unix.rs | 2 +- crates/ql_instances/src/download/libraries.rs | 1 + crates/ql_java_handler/src/alternate_java.rs | 2 +- crates/ql_java_handler/src/lib.rs | 2 +- quantum_launcher/src/menu_renderer/shortcut.rs | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/ezshortcut/src/os/unix.rs b/crates/ezshortcut/src/os/unix.rs index 9d31b27b7..01ec4c0ac 100644 --- a/crates/ezshortcut/src/os/unix.rs +++ b/crates/ezshortcut/src/os/unix.rs @@ -5,7 +5,7 @@ use std::{ use tokio::fs; -use crate::{make_filename_safe, Shortcut}; +use crate::Shortcut; pub fn get_menu_path() -> Option { if let Ok(dir) = std::env::var("XDG_DATA_HOME") { diff --git a/crates/ql_instances/src/download/libraries.rs b/crates/ql_instances/src/download/libraries.rs index 6029a52d7..ab5a9ddc7 100644 --- a/crates/ql_instances/src/download/libraries.rs +++ b/crates/ql_instances/src/download/libraries.rs @@ -231,6 +231,7 @@ impl GameDownloader { continue; } #[allow(unused)] + #[allow(clippy::let_and_return)] if !(OS_NAMES.iter().any(|os_name| { let os_name = format!("natives-{os_name}"); cfg_if!(if #[cfg(feature = "simulate_linux_arm64")] { diff --git a/crates/ql_java_handler/src/alternate_java.rs b/crates/ql_java_handler/src/alternate_java.rs index 77cb7828d..d116e85ef 100644 --- a/crates/ql_java_handler/src/alternate_java.rs +++ b/crates/ql_java_handler/src/alternate_java.rs @@ -78,7 +78,7 @@ async fn get_url(mut version: JavaVersion) -> Result, JavaInstall version = next; res = get_inner(version).await?; } - return Ok(res); + Ok(res) } #[derive(Deserialize)] diff --git a/crates/ql_java_handler/src/lib.rs b/crates/ql_java_handler/src/lib.rs index 63de76677..3da8bfbab 100644 --- a/crates/ql_java_handler/src/lib.rs +++ b/crates/ql_java_handler/src/lib.rs @@ -10,7 +10,7 @@ //! //! - ✅: [From Mojang](https://launchermeta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json) //! - 🟢: Supported through [Azul Zulu](https://www.azul.com/downloads/#zulu) -//! ([API](https://docs.azul.com/core/install/metadata-api)) +//! ([API](https://docs.azul.com/core/install/metadata-api)) //! - 🟢¹: Uses newer Java (with backwards compatibility) //! - 🟢²: Installed from: //! - FreeBSD: diff --git a/quantum_launcher/src/menu_renderer/shortcut.rs b/quantum_launcher/src/menu_renderer/shortcut.rs index baeb5ed9f..5e028fed7 100644 --- a/quantum_launcher/src/menu_renderer/shortcut.rs +++ b/quantum_launcher/src/menu_renderer/shortcut.rs @@ -53,7 +53,7 @@ impl MenuShortcut { .on_toggle(|t| Message::Shortcut(ShortcutMessage::ToggleAddToMenu(t))) .size(12) .text_size(12), - widget::checkbox(format!("Add to Desktop"), self.add_to_desktop) + widget::checkbox("Add to Desktop", self.add_to_desktop) .on_toggle(|t| Message::Shortcut(ShortcutMessage::ToggleAddToDesktop(t))) .size(12) .text_size(12), From 2f043e3df27a113fad8b690cc25da4b2d9bc0a88 Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Tue, 10 Feb 2026 20:20:16 +0530 Subject: [PATCH 10/30] Dev: Add saving to desktop --- crates/ezshortcut/src/lib.rs | 6 +- quantum_launcher/src/menu_renderer/mod.rs | 2 +- .../{shortcut.rs => shortcuts.rs} | 2 +- .../src/message_update/shortcuts.rs | 90 ++++++++++++------- quantum_launcher/src/state/message.rs | 1 + 5 files changed, 64 insertions(+), 37 deletions(-) rename quantum_launcher/src/menu_renderer/{shortcut.rs => shortcuts.rs} (98%) diff --git a/crates/ezshortcut/src/lib.rs b/crates/ezshortcut/src/lib.rs index fd232860e..428d017dc 100644 --- a/crates/ezshortcut/src/lib.rs +++ b/crates/ezshortcut/src/lib.rs @@ -1,8 +1,12 @@ -use std::path::Path; +use std::path::{Path, PathBuf}; mod os; pub use os::{get_menu_path, EXTENSION}; +pub fn get_desktop_dir() -> Option { + dirs::desktop_dir().or_else(|| dirs::home_dir().map(|n| n.join("Desktop"))) +} + #[derive(Debug, Clone)] pub struct Shortcut { pub name: String, diff --git a/quantum_launcher/src/menu_renderer/mod.rs b/quantum_launcher/src/menu_renderer/mod.rs index f9e24ba6a..821a946b4 100644 --- a/quantum_launcher/src/menu_renderer/mod.rs +++ b/quantum_launcher/src/menu_renderer/mod.rs @@ -24,7 +24,7 @@ mod login; mod mods; mod onboarding; mod settings; -mod shortcut; +mod shortcuts; pub use onboarding::changelog; diff --git a/quantum_launcher/src/menu_renderer/shortcut.rs b/quantum_launcher/src/menu_renderer/shortcuts.rs similarity index 98% rename from quantum_launcher/src/menu_renderer/shortcut.rs rename to quantum_launcher/src/menu_renderer/shortcuts.rs index 5e028fed7..7cac32af3 100644 --- a/quantum_launcher/src/menu_renderer/shortcut.rs +++ b/quantum_launcher/src/menu_renderer/shortcuts.rs @@ -47,7 +47,7 @@ impl MenuShortcut { self.get_info_fields(accounts), row![ - button_with_icon(icons::checkmark_s(14), "Create Shortcut", 14).on_press(Message::Nothing), + button_with_icon(icons::checkmark_s(14), "Create Shortcut", 14).on_press(Message::Shortcut(ShortcutMessage::SaveMenu)), column![ widget::checkbox(format!("Add to {MENU_NAME}"), self.add_to_menu) .on_toggle(|t| Message::Shortcut(ShortcutMessage::ToggleAddToMenu(t))) diff --git a/quantum_launcher/src/message_update/shortcuts.rs b/quantum_launcher/src/message_update/shortcuts.rs index 85b595d53..92826936f 100644 --- a/quantum_launcher/src/message_update/shortcuts.rs +++ b/quantum_launcher/src/message_update/shortcuts.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use ezshortcut::Shortcut; use iced::Task; use ql_core::{info, IntoStringError}; @@ -76,43 +78,21 @@ impl Launcher { }, ); }), - ShortcutMessage::SaveCustomPicked(f) => iflet!(menu, self.state, { - let mut shortcut = menu.shortcut.clone(); - let instance = self.selected_instance.as_ref().unwrap(); - - let exec_path = match std::env::current_exe() { - Ok(n) => n, - Err(err) => { - self.set_error(format!("while getting path to current exe:\n{err}")); + ShortcutMessage::SaveCustomPicked(path) => return self.shortcut_generate(path), + ShortcutMessage::SaveMenu => iflet!(menu, &mut self.state, { + let t_desktop = if menu.add_to_desktop { + let Some(desktop) = ezshortcut::get_desktop_dir() else { + self.set_error("Couldn't access Desktop folder"); return Task::none(); - } + }; + self.shortcut_generate(desktop) + } else { + Task::none() }; - let launch = format!( - "{exe} {server}launch {name} {username}{login} --show-progress", - exe = exec_path.to_string_lossy(), - server = if instance.is_server() { - "--enable-server-manager -s " - } else { - "" - }, - name = serde_json::to_string(&instance.get_name()) - .unwrap_or_else(|_| instance.get_name().to_owned()), - username = if menu.account == OFFLINE_ACCOUNT_NAME { - &menu.account_offline - } else { - &menu.account - }, - login = if menu.account == OFFLINE_ACCOUNT_NAME { - "" - } else { - " -u" - } - ); - shortcut.exec = launch; - return Task::perform(async move { shortcut.generate(&f).await }, |n| { - Message::Shortcut(ShortcutMessage::Done(n.strerr())) - }); + let t_menu = Task::none(); // TODO + + return Task::batch([t_desktop, t_menu]); }), ShortcutMessage::Done(r) => match r { Ok(()) => { @@ -124,4 +104,46 @@ impl Launcher { } Task::none() } + + pub fn shortcut_generate(&mut self, path: PathBuf) -> Task { + let State::CreateShortcut(menu) = &self.state else { + return Task::none(); + }; + let mut shortcut = menu.shortcut.clone(); + let instance = self.selected_instance.as_ref().unwrap(); + + let exec_path = match std::env::current_exe() { + Ok(n) => n, + Err(err) => { + self.set_error(format!("while getting path to current exe:\n{err}")); + return Task::none(); + } + }; + let launch = format!( + "{exe} {server}launch {name} {username}{login} --show-progress", + exe = exec_path.to_string_lossy(), + server = if instance.is_server() { + "--enable-server-manager -s " + } else { + "" + }, + name = serde_json::to_string(&instance.get_name()) + .unwrap_or_else(|_| instance.get_name().to_owned()), + username = if menu.account == OFFLINE_ACCOUNT_NAME { + &menu.account_offline + } else { + &menu.account + }, + login = if menu.account == OFFLINE_ACCOUNT_NAME { + "" + } else { + " -u" + } + ); + shortcut.exec = launch; + + Task::perform(async move { shortcut.generate(&path).await }, |n| { + Message::Shortcut(ShortcutMessage::Done(n.strerr())) + }) + } } diff --git a/quantum_launcher/src/state/message.rs b/quantum_launcher/src/state/message.rs index 48afc4c98..933705e04 100644 --- a/quantum_launcher/src/state/message.rs +++ b/quantum_launcher/src/state/message.rs @@ -366,6 +366,7 @@ pub enum ShortcutMessage { SaveCustom, SaveCustomPicked(PathBuf), + SaveMenu, Done(Res), } From fb465f9cb658490a8f36723a7eecb63eadcccacd Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Tue, 10 Feb 2026 21:54:42 +0530 Subject: [PATCH 11/30] Dev: Add saving to application menu --- crates/ezshortcut/src/lib.rs | 13 ++ crates/ezshortcut/src/os/macos.rs | 9 ++ crates/ezshortcut/src/os/stub.rs | 7 + crates/ezshortcut/src/os/unix.rs | 21 +++ crates/ezshortcut/src/os/windows.rs | 9 ++ .../src/message_update/shortcuts.rs | 124 ++++++++++-------- quantum_launcher/src/update.rs | 5 +- 7 files changed, 135 insertions(+), 53 deletions(-) diff --git a/crates/ezshortcut/src/lib.rs b/crates/ezshortcut/src/lib.rs index 428d017dc..b31b59dc5 100644 --- a/crates/ezshortcut/src/lib.rs +++ b/crates/ezshortcut/src/lib.rs @@ -17,10 +17,23 @@ pub struct Shortcut { } impl Shortcut { + /// Create the shortcut and save it to `path`. + /// + /// Note: On Linux, if you just place the shortcut anywhere, + /// it may not work. You may need to save it to system-wide locations + /// (see [`Self::generate_to_applications`]). pub async fn generate(&self, path: &Path) -> std::io::Result<()> { os::create(self, path).await } + /// Creates the shortcut and adds it to Start Menu/Applications/Application Menu. + /// + /// This also *tries* to refresh the application list, it may take a while to update though. + pub async fn generate_to_applications(&self) -> std::io::Result<()> { + os::create_in_applications(self).await + } + + /// Gets the recommended filename for the shortcut based on OS behavior. pub fn get_filename(&self) -> String { let mut filtered_name = make_filename_safe(&self.name, !cfg!(target_os = "windows")); filtered_name.push_str(EXTENSION); diff --git a/crates/ezshortcut/src/os/macos.rs b/crates/ezshortcut/src/os/macos.rs index d4f1f42bb..06a8ebdb2 100644 --- a/crates/ezshortcut/src/os/macos.rs +++ b/crates/ezshortcut/src/os/macos.rs @@ -5,9 +5,18 @@ pub fn get_menu_path() -> Option { dirs::home_dir().map(|h| h.join("Applications")) } +fn refresh_applications() {} + pub async fn create(shortcut: &Shortcut, path: impl AsRef) -> std::io::Result<()> { Err(std::io::Error::new( std::io::ErrorKind::Unsupported, "Shortcuts aren't supported on this platform", )) } + +pub async fn create_in_applications(shortcut: &Shortcut) -> std::io::Result<()> { + Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "Shortcuts aren't supported on this platform", + )) +} diff --git a/crates/ezshortcut/src/os/stub.rs b/crates/ezshortcut/src/os/stub.rs index aa4aecfb5..eb20fb7ab 100644 --- a/crates/ezshortcut/src/os/stub.rs +++ b/crates/ezshortcut/src/os/stub.rs @@ -11,3 +11,10 @@ pub async fn create(shortcut: &Shortcut, path: impl AsRef) -> std::io::Res "Shortcuts aren't supported on this platform", )) } + +pub async fn create_in_applications(shortcut: &Shortcut) -> std::io::Result<()> { + Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "Shortcuts aren't supported on this platform", + )) +} diff --git a/crates/ezshortcut/src/os/unix.rs b/crates/ezshortcut/src/os/unix.rs index 01ec4c0ac..604579598 100644 --- a/crates/ezshortcut/src/os/unix.rs +++ b/crates/ezshortcut/src/os/unix.rs @@ -15,6 +15,27 @@ pub fn get_menu_path() -> Option { } } +fn refresh_applications() { + tokio::task::spawn(async { + _ = tokio::process::Command::new("update-desktop-database") + .output() + .await; + _ = tokio::process::Command::new("kbuildsycoca5").output().await; + }); +} + +pub async fn create_in_applications(shortcut: &Shortcut) -> std::io::Result<()> { + let path = dirs::data_dir() + .ok_or(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Couldn't access data dir (.local/share)", + ))? + .join("applications"); + create(shortcut, path).await?; + refresh_applications(); + Ok(()) +} + pub async fn create(shortcut: &Shortcut, path: impl AsRef) -> std::io::Result<()> { let path = path.as_ref(); create_inner(shortcut, path).await?; diff --git a/crates/ezshortcut/src/os/windows.rs b/crates/ezshortcut/src/os/windows.rs index 581ec96cc..a250727f2 100644 --- a/crates/ezshortcut/src/os/windows.rs +++ b/crates/ezshortcut/src/os/windows.rs @@ -5,9 +5,18 @@ pub fn get_menu_path() -> Option { dirs::data_dir().map(|d| d.join("Microsoft/Windows/Start Menu/Programs")) } +fn refresh_applications() {} + pub async fn create(shortcut: &Shortcut, path: impl AsRef) -> std::io::Result<()> { Err(std::io::Error::new( std::io::ErrorKind::Unsupported, "Shortcuts aren't supported on this platform", )) } + +pub async fn create_in_applications(shortcut: &Shortcut) -> std::io::Result<()> { + Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "Shortcuts aren't supported on this platform", + )) +} diff --git a/quantum_launcher/src/message_update/shortcuts.rs b/quantum_launcher/src/message_update/shortcuts.rs index 92826936f..fb997189d 100644 --- a/quantum_launcher/src/message_update/shortcuts.rs +++ b/quantum_launcher/src/message_update/shortcuts.rs @@ -1,5 +1,3 @@ -use std::path::PathBuf; - use ezshortcut::Shortcut; use iced::Task; use ql_core::{info, IntoStringError}; @@ -17,21 +15,10 @@ macro_rules! iflet { } impl Launcher { - pub fn update_shortcut(&mut self, msg: ShortcutMessage) -> Task { + pub fn update_shortcut(&mut self, msg: ShortcutMessage) -> Result, String> { match msg { ShortcutMessage::Open => { - self.state = State::CreateShortcut(MenuShortcut { - shortcut: Shortcut { - name: self.instance().get_name().to_owned(), - description: "".to_owned(), - exec: String::new(), - icon: None, - }, - add_to_menu: true, - add_to_desktop: false, - account: self.account_selected.clone(), - account_offline: self.config.username.clone(), - }) + self.shortcut_open(); } ShortcutMessage::ToggleAddToMenu(t) => iflet!(menu, self.state, { if t || menu.add_to_desktop { @@ -61,7 +48,7 @@ impl Launcher { }), ShortcutMessage::SaveCustom => iflet!(menu, self.state, { - return Task::perform( + return Ok(Task::perform( rfd::AsyncFileDialog::new() .add_filter("Shortcut", &[ezshortcut::EXTENSION]) .set_file_name(menu.shortcut.get_filename()) @@ -76,49 +63,67 @@ impl Launcher { Message::Nothing } }, - ); - }), - ShortcutMessage::SaveCustomPicked(path) => return self.shortcut_generate(path), - ShortcutMessage::SaveMenu => iflet!(menu, &mut self.state, { - let t_desktop = if menu.add_to_desktop { - let Some(desktop) = ezshortcut::get_desktop_dir() else { - self.set_error("Couldn't access Desktop folder"); - return Task::none(); - }; - self.shortcut_generate(desktop) - } else { - Task::none() - }; - - let t_menu = Task::none(); // TODO - - return Task::batch([t_desktop, t_menu]); + )); }), - ShortcutMessage::Done(r) => match r { - Ok(()) => { - info!("Created instance shortcut"); - // TODO: keep track of created shortcuts + ShortcutMessage::SaveCustomPicked(path) => { + let shortcut = self.shortcut_prepare()?; + return Ok(Task::perform( + async move { shortcut.generate(&path).await }, + |n| Message::Shortcut(ShortcutMessage::Done(n.strerr())), + )); + } + ShortcutMessage::SaveMenu => { + let shortcut = self.shortcut_prepare()?; + if let State::CreateShortcut(menu) = &self.state { + return Ok(Task::batch([ + if menu.add_to_desktop { + shortcut_desktop(&shortcut)? + } else { + Task::none() + }, + if menu.add_to_menu { + shortcut_menu(shortcut) + } else { + Task::none() + }, + ])); } - Err(err) => self.set_error(err), - }, + } + ShortcutMessage::Done(result) => { + result?; + info!("Created shortcut"); + // TODO: keep track of created shortcuts + } } - Task::none() + Ok(Task::none()) } - pub fn shortcut_generate(&mut self, path: PathBuf) -> Task { + fn shortcut_open(&mut self) { + self.state = State::CreateShortcut(MenuShortcut { + shortcut: Shortcut { + name: self.instance().get_name().to_owned(), + description: "".to_owned(), + exec: String::new(), + icon: None, + }, + add_to_menu: true, + add_to_desktop: false, + account: self.account_selected.clone(), + account_offline: self.config.username.clone(), + }) + } + + pub fn shortcut_prepare(&mut self) -> Result { let State::CreateShortcut(menu) = &self.state else { - return Task::none(); + self.shortcut_open(); + return self.shortcut_prepare(); }; let mut shortcut = menu.shortcut.clone(); let instance = self.selected_instance.as_ref().unwrap(); - let exec_path = match std::env::current_exe() { - Ok(n) => n, - Err(err) => { - self.set_error(format!("while getting path to current exe:\n{err}")); - return Task::none(); - } - }; + let exec_path = std::env::current_exe() + .map_err(|n| format!("while getting path to current exe:\n{n}"))?; + let launch = format!( "{exe} {server}launch {name} {username}{login} --show-progress", exe = exec_path.to_string_lossy(), @@ -142,8 +147,23 @@ impl Launcher { ); shortcut.exec = launch; - Task::perform(async move { shortcut.generate(&path).await }, |n| { - Message::Shortcut(ShortcutMessage::Done(n.strerr())) - }) + Ok(shortcut) } } + +fn shortcut_menu(shortcut: Shortcut) -> Task { + Task::perform( + async move { shortcut.generate_to_applications().await }, + |n| Message::Shortcut(ShortcutMessage::Done(n.strerr())), + ) +} + +fn shortcut_desktop(shortcut: &Shortcut) -> Result, String> { + let desktop = + ezshortcut::get_desktop_dir().ok_or_else(|| "Couldn't access Desktop folder".to_owned())?; + let s = shortcut.clone(); + Ok(Task::perform( + async move { s.generate(&desktop).await }, + |n| Message::Shortcut(ShortcutMessage::Done(n.strerr())), + )) +} diff --git a/quantum_launcher/src/update.rs b/quantum_launcher/src/update.rs index 5e4ee76a6..9215ee6fa 100644 --- a/quantum_launcher/src/update.rs +++ b/quantum_launcher/src/update.rs @@ -69,7 +69,10 @@ impl Launcher { Message::Window(msg) => return self.update_window_msg(msg), Message::Notes(msg) => return self.update_notes(msg), Message::GameLog(msg) => return self.update_game_log(msg), - Message::Shortcut(msg) => return self.update_shortcut(msg), + Message::Shortcut(msg) => match self.update_shortcut(msg) { + Ok(n) => return n, + Err(e) => self.set_error(e), + }, Message::LaunchInstanceSelected(inst) => { self.selected_instance = Some(inst); From 1b4e60c4931c234a70cf2e59b42c90cde8cc79a0 Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Wed, 18 Feb 2026 01:22:17 -0800 Subject: [PATCH 12/30] Dev: store shortcut args in a more structured way --- crates/ezshortcut/src/lib.rs | 10 +++++ crates/ezshortcut/src/os/unix.rs | 3 +- .../src/message_update/shortcuts.rs | 40 +++++++++---------- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/crates/ezshortcut/src/lib.rs b/crates/ezshortcut/src/lib.rs index b31b59dc5..f415d80fd 100644 --- a/crates/ezshortcut/src/lib.rs +++ b/crates/ezshortcut/src/lib.rs @@ -13,6 +13,7 @@ pub struct Shortcut { /// Linux/BSD/Unix only, leave blank for none pub description: String, pub exec: String, + pub exec_args: Vec, pub icon: Option, } @@ -39,6 +40,15 @@ impl Shortcut { filtered_name.push_str(EXTENSION); filtered_name } + + fn get_formatted_args(&self) -> String { + let mut s = String::new(); + for e in self.exec_args.iter().map(|n| format!("{n:?}")) { + s.push_str(&e); + s.push(' '); + } + s + } } fn make_filename_safe(input: &str, remove_spaces: bool) -> String { diff --git a/crates/ezshortcut/src/os/unix.rs b/crates/ezshortcut/src/os/unix.rs index 604579598..665e4f5ce 100644 --- a/crates/ezshortcut/src/os/unix.rs +++ b/crates/ezshortcut/src/os/unix.rs @@ -45,12 +45,13 @@ pub async fn create(shortcut: &Shortcut, path: impl AsRef) -> std::io::Res async fn create_inner(shortcut: &Shortcut, path: &Path) -> Result<(), std::io::Error> { let desc = shortcut.description.trim(); + let args: String = shortcut.get_formatted_args(); let content = format!( r"[Desktop Entry] Version=1.0 Type=Application Name={name} -{icon}{description}Exec={exec} +{icon}{description}Exec={exec} {args} Terminal=false Categories=Game;", name = shortcut.name, diff --git a/quantum_launcher/src/message_update/shortcuts.rs b/quantum_launcher/src/message_update/shortcuts.rs index fb997189d..10f179fb2 100644 --- a/quantum_launcher/src/message_update/shortcuts.rs +++ b/quantum_launcher/src/message_update/shortcuts.rs @@ -104,6 +104,7 @@ impl Launcher { name: self.instance().get_name().to_owned(), description: "".to_owned(), exec: String::new(), + exec_args: vec![], icon: None, }, add_to_menu: true, @@ -124,28 +125,23 @@ impl Launcher { let exec_path = std::env::current_exe() .map_err(|n| format!("while getting path to current exe:\n{n}"))?; - let launch = format!( - "{exe} {server}launch {name} {username}{login} --show-progress", - exe = exec_path.to_string_lossy(), - server = if instance.is_server() { - "--enable-server-manager -s " - } else { - "" - }, - name = serde_json::to_string(&instance.get_name()) - .unwrap_or_else(|_| instance.get_name().to_owned()), - username = if menu.account == OFFLINE_ACCOUNT_NAME { - &menu.account_offline - } else { - &menu.account - }, - login = if menu.account == OFFLINE_ACCOUNT_NAME { - "" - } else { - " -u" - } - ); - shortcut.exec = launch; + shortcut.exec = exec_path.to_string_lossy().to_string(); + if instance.is_server() { + shortcut + .exec_args + .push("--enable-server-manager".to_owned()); + shortcut.exec_args.push("-s".to_owned()); + } + shortcut + .exec_args + .extend(["launch".to_owned(), instance.get_name().to_owned()]); + if menu.account == OFFLINE_ACCOUNT_NAME { + shortcut.exec_args.push(menu.account_offline.clone()); + } else { + shortcut.exec_args.push(menu.account.clone()); + shortcut.exec_args.push("-u".to_owned()); + } + shortcut.exec_args.push("--show-progress".to_owned()); Ok(shortcut) } From d89e9537475349f1b09682c032d7a060cd3dd267 Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Wed, 18 Feb 2026 01:22:48 -0800 Subject: [PATCH 13/30] Fix: avoid using notify_rust on macOS (doesnt work on old macOS), use osascript instead --- quantum_launcher/Cargo.toml | 2 ++ quantum_launcher/src/cli/command.rs | 7 ++----- quantum_launcher/src/cli/mod.rs | 26 ++++++++++++++++++++------ 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/quantum_launcher/Cargo.toml b/quantum_launcher/Cargo.toml index dc51803c3..b922520dd 100644 --- a/quantum_launcher/Cargo.toml +++ b/quantum_launcher/Cargo.toml @@ -79,6 +79,8 @@ terminal_size = "0.4" notify = "8" open = "5.3.2" dark-light = "2" + +[target.'cfg(not(target_os = "macos"))'.dependencies] notify-rust = { version = "4", default-features = false, features = ["z-with-tokio"] } # iced::widget::markdown doesn't have inline diff --git a/quantum_launcher/src/cli/command.rs b/quantum_launcher/src/cli/command.rs index e8fbfb30f..4d265cee1 100644 --- a/quantum_launcher/src/cli/command.rs +++ b/quantum_launcher/src/cli/command.rs @@ -9,7 +9,7 @@ use ql_mod_manager::loaders::LoaderInstallResult; use std::{path::PathBuf, process::exit}; use crate::{ - cli::{helpers::render_row, QLoader}, + cli::{helpers::render_row, show_notification, QLoader}, config::LauncherConfig, state::get_entries, }; @@ -260,10 +260,7 @@ async fn refresh_account( if show_progress { tokio::task::spawn_blocking(|| { - notify_rust::Notification::new() - .summary("Launching game") - .body("Refreshing account...") - .show() + show_notification("Launching game", "Refreshing account..."); }); }; diff --git a/quantum_launcher/src/cli/mod.rs b/quantum_launcher/src/cli/mod.rs index 4bc0865e1..1f78acef3 100644 --- a/quantum_launcher/src/cli/mod.rs +++ b/quantum_launcher/src/cli/mod.rs @@ -231,7 +231,7 @@ pub fn start_cli(is_dir_err: bool) { std::process::exit(if let Err(err) = res { err!("{err}"); if show_progress { - show_notification(err.to_string()); + show_notification("Error launching game", &err.to_string()); } 1 } else { @@ -259,11 +259,25 @@ pub fn start_cli(is_dir_err: bool) { } } -fn show_notification(err: String) { - _ = notify_rust::Notification::new() - .summary("Error launching game") - .body(&err) - .show(); +fn show_notification(title: &str, body: &str) { + #[cfg(not(target_os = "macos"))] + { + _ = notify_rust::Notification::new() + .summary(title) + .body(err) + .show(); + } + #[cfg(target_os = "macos")] + { + _ = std::process::Command::new("osascript") + .args([ + "-e", + &format!("display notification {body:?} with title {title:?}"), + "-e", + "delay 5", + ]) + .spawn(); + } } fn quit(res: Result<(), Box>) { From 6f8b6456c459dfa22db01be27e041c208e3c7473 Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Wed, 18 Feb 2026 01:23:07 -0800 Subject: [PATCH 14/30] Dev: add macOS backend to shortcuts --- crates/ezshortcut/src/os/macos.rs | 97 +++++++++++++++++++++++++++---- 1 file changed, 87 insertions(+), 10 deletions(-) diff --git a/crates/ezshortcut/src/os/macos.rs b/crates/ezshortcut/src/os/macos.rs index 06a8ebdb2..b6f8c7d84 100644 --- a/crates/ezshortcut/src/os/macos.rs +++ b/crates/ezshortcut/src/os/macos.rs @@ -1,22 +1,99 @@ -use crate::Shortcut; +use tokio::{fs, process::Command}; + +use crate::{make_filename_safe, Shortcut}; use std::path::{Path, PathBuf}; pub fn get_menu_path() -> Option { dirs::home_dir().map(|h| h.join("Applications")) } -fn refresh_applications() {} +fn refresh_applications() { + tokio::task::spawn(async { + _ = Command::new("mdimport").arg("/Applications/").spawn(); + }); +} pub async fn create(shortcut: &Shortcut, path: impl AsRef) -> std::io::Result<()> { - Err(std::io::Error::new( - std::io::ErrorKind::Unsupported, - "Shortcuts aren't supported on this platform", - )) + create_inner(shortcut, path.as_ref()).await?; + Ok(()) +} + +async fn create_inner(shortcut: &Shortcut, path: &Path) -> std::io::Result { + let path = if path.extension().is_some_and(|n| n == ".app") { + path.to_owned() + } else { + path.join(shortcut.get_filename()) + }; + + fs::create_dir_all(&path).await?; + let contents = path.join("Contents"); + let exec_dir = contents.join("MacOS"); + fs::create_dir_all(&contents).await?; + fs::create_dir_all(&exec_dir).await?; + fs::create_dir_all(&contents.join("Resources")).await?; + + let exec_path = exec_dir.join("ql_shortcut"); + + let script = format!( + r#"#!/usr/bin/env sh + +exec {:?} {} +"#, + shortcut.exec, + shortcut.get_formatted_args() + ); + fs::write(&exec_path, &script).await?; + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(&exec_path, std::fs::Permissions::from_mode(0o755)).await?; + + let info_plist = format!( + r#" + + + + CFBundleExecutable + ql_shortcut + CFBundleIdentifier + io.github.Mrmayman.QLShortcut_{} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + {} + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.1.0 + CFBundleVersion + 0.1.0 + NSPrincipalClass + NSApplication + + +"#, + shortcut.name, + make_filename_safe(&shortcut.name, true), + ); + // TODO: Before the you could have + // ``` + // CFBundleIconFile + // ql_logo + // ``` + // with icon in Contents/Resources/ql_logo.icns + // May need iconutil and sips for this + + let info_path = contents.join("Info.plist"); + fs::write(&info_path, &info_plist).await?; + + _ = Command::new("xattr").arg("-rc").arg(&path).spawn(); + + Ok(path) } pub async fn create_in_applications(shortcut: &Shortcut) -> std::io::Result<()> { - Err(std::io::Error::new( - std::io::ErrorKind::Unsupported, - "Shortcuts aren't supported on this platform", - )) + let path = Path::new("/Applications"); + let shortcut_path = create_inner(shortcut, path).await?; + _ = Command::new("open").arg("-R").arg(&shortcut_path).spawn(); + refresh_applications(); + Ok(()) } From 2ce316328571e768c534a65eb2a7839b5e2fe91a Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Wed, 18 Feb 2026 14:57:27 +0530 Subject: [PATCH 15/30] Fix build error --- quantum_launcher/src/cli/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantum_launcher/src/cli/mod.rs b/quantum_launcher/src/cli/mod.rs index 1f78acef3..1571304e1 100644 --- a/quantum_launcher/src/cli/mod.rs +++ b/quantum_launcher/src/cli/mod.rs @@ -264,7 +264,7 @@ fn show_notification(title: &str, body: &str) { { _ = notify_rust::Notification::new() .summary(title) - .body(err) + .body(body) .show(); } #[cfg(target_os = "macos")] From 10635eb57c6502375b003458314d7bd1de3aa3b5 Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Wed, 18 Feb 2026 17:58:22 +0530 Subject: [PATCH 16/30] Dev: Add windows backend --- Cargo.lock | 1 + crates/ezshortcut/Cargo.toml | 9 ++ crates/ezshortcut/src/lib.rs | 14 +- crates/ezshortcut/src/os/windows.rs | 128 ++++++++++++++++-- quantum_launcher/Cargo.toml | 5 +- .../src/menu_renderer/shortcuts.rs | 8 +- 6 files changed, 138 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c949b6e44..dded7a8a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1475,6 +1475,7 @@ dependencies = [ "cfg-if", "dirs", "tokio", + "windows 0.61.3", ] [[package]] diff --git a/crates/ezshortcut/Cargo.toml b/crates/ezshortcut/Cargo.toml index 13e3a7fa9..25d5b5c24 100644 --- a/crates/ezshortcut/Cargo.toml +++ b/crates/ezshortcut/Cargo.toml @@ -7,3 +7,12 @@ edition = "2021" cfg-if.workspace = true tokio.workspace = true dirs.workspace = true + +# LNK shortcut generation +# Don't forget, also used by quantum_launcher crate +# If you update this update that too +[target.'cfg(target_os = "windows")'.dependencies] +windows = { version = "0.61", features = [ + "Win32_UI_Shell", + "Win32_System_Com", +] } diff --git a/crates/ezshortcut/src/lib.rs b/crates/ezshortcut/src/lib.rs index f415d80fd..9482dbe08 100644 --- a/crates/ezshortcut/src/lib.rs +++ b/crates/ezshortcut/src/lib.rs @@ -10,7 +10,7 @@ pub fn get_desktop_dir() -> Option { #[derive(Debug, Clone)] pub struct Shortcut { pub name: String, - /// Linux/BSD/Unix only, leave blank for none + /// Leave blank for none. Unsupported on macOS pub description: String, pub exec: String, pub exec_args: Vec, @@ -41,13 +41,13 @@ impl Shortcut { filtered_name } + #[cfg(target_family = "unix")] fn get_formatted_args(&self) -> String { - let mut s = String::new(); - for e in self.exec_args.iter().map(|n| format!("{n:?}")) { - s.push_str(&e); - s.push(' '); - } - s + self.exec_args + .iter() + .map(|n| format!("{n:?}")) + .collect::>() + .join(" ") } } diff --git a/crates/ezshortcut/src/os/windows.rs b/crates/ezshortcut/src/os/windows.rs index a250727f2..eac34a3ca 100644 --- a/crates/ezshortcut/src/os/windows.rs +++ b/crates/ezshortcut/src/os/windows.rs @@ -1,22 +1,124 @@ +use tokio::fs; +use windows::{ + core::{Interface, HSTRING}, + Win32::{ + System::Com::{ + CoCreateInstance, CoInitializeEx, CoTaskMemFree, CoUninitialize, IPersistFile, + CLSCTX_INPROC_SERVER, COINIT_APARTMENTTHREADED, + }, + UI::Shell::{ + FOLDERID_Programs, IShellLinkW, SHGetKnownFolderPath, ShellLink, KNOWN_FOLDER_FLAG, + }, + }, +}; + use crate::Shortcut; -use std::path::{Path, PathBuf}; +use std::path::Path; -pub fn get_menu_path() -> Option { - dirs::data_dir().map(|d| d.join("Microsoft/Windows/Start Menu/Programs")) -} +pub fn get_menu_path() -> Option { + unsafe { + let path_ptr = + SHGetKnownFolderPath(&FOLDERID_Programs, KNOWN_FOLDER_FLAG::default(), None).ok()?; -fn refresh_applications() {} + let path = path_ptr.to_string().ok()?; + CoTaskMemFree(Some(path_ptr.0 as _)); + Some(std::path::PathBuf::from(path).join("QuantumLauncher")) + } +} pub async fn create(shortcut: &Shortcut, path: impl AsRef) -> std::io::Result<()> { - Err(std::io::Error::new( - std::io::ErrorKind::Unsupported, - "Shortcuts aren't supported on this platform", - )) + create_inner(shortcut, path.as_ref()).await?; + Ok(()) +} + +async fn create_inner(shortcut: &Shortcut, path: &Path) -> std::io::Result<()> { + let path = match fs::metadata(path).await { + Ok(n) if n.is_dir() => path.join(shortcut.get_filename()), + _ => path.to_owned(), + }; + + let args = shortcut + .exec_args + .iter() + .map(|a| quote_windows_arg(a)) + .collect::>() + .join(" "); + + unsafe { + CoInitializeEx(None, COINIT_APARTMENTTHREADED) + .ok() + .map_err(ioerr)?; + + let shell_link: IShellLinkW = CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER)?; + shell_link.SetPath(&HSTRING::from(&shortcut.exec))?; + if !args.trim().is_empty() { + shell_link.SetArguments(&HSTRING::from(args))?; + } + if !shortcut.description.trim().is_empty() { + shell_link.SetDescription(&HSTRING::from(&shortcut.description))?; + } + + let persist: IPersistFile = shell_link.cast()?; + persist.Save(&HSTRING::from(path.as_path()), true)?; + + CoUninitialize(); + } + + Ok(()) +} + +fn ioerr(err: impl std::error::Error + Send + Sync + 'static) -> std::io::Error { + std::io::Error::new(std::io::ErrorKind::Other, Box::new(err)) } pub async fn create_in_applications(shortcut: &Shortcut) -> std::io::Result<()> { - Err(std::io::Error::new( - std::io::ErrorKind::Unsupported, - "Shortcuts aren't supported on this platform", - )) + let start_menu = tokio::task::spawn_blocking(|| get_menu_path()) + .await? + .ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::NotFound, "Start menu path not found") + })?; + fs::create_dir_all(&start_menu).await?; + create_inner(shortcut, &start_menu).await +} + +fn quote_windows_arg(arg: &str) -> String { + if arg.is_empty() { + return "\"\"".to_string(); + } + + let needs_quotes = arg.chars().any(|c| c == ' ' || c == '\t' || c == '"'); + + if !needs_quotes { + return arg.to_string(); + } + + let mut result = String::from("\""); + let mut backslashes = 0; + + for ch in arg.chars() { + match ch { + '\\' => { + backslashes += 1; + } + '"' => { + result.push_str(&"\\".repeat(backslashes * 2 + 1)); + result.push('"'); + backslashes = 0; + } + _ => { + if backslashes > 0 { + result.push_str(&"\\".repeat(backslashes)); + backslashes = 0; + } + result.push(ch); + } + } + } + + if backslashes > 0 { + result.push_str(&"\\".repeat(backslashes * 2)); + } + + result.push('"'); + result } diff --git a/quantum_launcher/Cargo.toml b/quantum_launcher/Cargo.toml index b922520dd..4df8c2bf3 100644 --- a/quantum_launcher/Cargo.toml +++ b/quantum_launcher/Cargo.toml @@ -83,8 +83,7 @@ dark-light = "2" [target.'cfg(not(target_os = "macos"))'.dependencies] notify-rust = { version = "4", default-features = false, features = ["z-with-tokio"] } -# iced::widget::markdown doesn't have inline -# HTML support, so I made my own renderer. - +# Don't forget, also used by ezshortcut crate +# If you update this update that too [target.'cfg(target_os = "windows")'.dependencies] windows = { version = "0.61", features = ["Win32_System_Console"] } diff --git a/quantum_launcher/src/menu_renderer/shortcuts.rs b/quantum_launcher/src/menu_renderer/shortcuts.rs index 7cac32af3..937b8ecbe 100644 --- a/quantum_launcher/src/menu_renderer/shortcuts.rs +++ b/quantum_launcher/src/menu_renderer/shortcuts.rs @@ -12,13 +12,13 @@ use iced::{ cfg_if!(if #[cfg(target_os = "windows")] { const MENU_NAME: &str = "the Start Menu"; - const IS_UNIX: bool = false; + const SHOW_DESC: bool = true; } else if #[cfg(target_os = "macos")] { const MENU_NAME: &str = "Applications"; - const IS_UNIX: bool = false; + const SHOW_DESC: bool = false; } else { const MENU_NAME: &str = "the Applications Menu"; - const IS_UNIX: bool = true; + const SHOW_DESC: bool = true; }); impl MenuShortcut { @@ -111,7 +111,7 @@ impl MenuShortcut { .size(14) .on_input(|n| Message::Shortcut(ShortcutMessage::EditName(n))) )] - .push_maybe(IS_UNIX.then(|| { + .push_maybe(SHOW_DESC.then(|| { ifield( " Description:", widget::text_input("Leave blank for none", &self.shortcut.description) From 2ce081eec60cc994db795b9a5cbdbd34ce8f9929 Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Wed, 18 Feb 2026 17:58:43 +0530 Subject: [PATCH 17/30] Dev: Show shortcut launching notification even without auth refresh --- quantum_launcher/src/cli/command.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/quantum_launcher/src/cli/command.rs b/quantum_launcher/src/cli/command.rs index 4d265cee1..cb54381fc 100644 --- a/quantum_launcher/src/cli/command.rs +++ b/quantum_launcher/src/cli/command.rs @@ -288,6 +288,11 @@ async fn refresh_account( } } } else { + if show_progress { + tokio::task::spawn_blocking(|| { + show_notification("Launching game", "Enjoy!"); + }); + }; None }) } From 52d00020f72826baf393f80e1a14e8a57fa5f058 Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Wed, 18 Feb 2026 18:31:17 +0530 Subject: [PATCH 18/30] Dev: Improve UI design, add open start menu folder button instead of keeping list of every shortcut file --- .../src/menu_renderer/shortcuts.rs | 117 +++++++++--------- .../src/message_update/shortcuts.rs | 5 + quantum_launcher/src/state/message.rs | 1 + 3 files changed, 65 insertions(+), 58 deletions(-) diff --git a/quantum_launcher/src/menu_renderer/shortcuts.rs b/quantum_launcher/src/menu_renderer/shortcuts.rs index 937b8ecbe..aff1eeb0d 100644 --- a/quantum_launcher/src/menu_renderer/shortcuts.rs +++ b/quantum_launcher/src/menu_renderer/shortcuts.rs @@ -13,82 +13,82 @@ use iced::{ cfg_if!(if #[cfg(target_os = "windows")] { const MENU_NAME: &str = "the Start Menu"; const SHOW_DESC: bool = true; + const BWIDTH: u16 = 120; } else if #[cfg(target_os = "macos")] { const MENU_NAME: &str = "Applications"; const SHOW_DESC: bool = false; + const BWIDTH: u16 = 140; } else { const MENU_NAME: &str = "the Applications Menu"; const SHOW_DESC: bool = true; + const BWIDTH: u16 = 150; }); impl MenuShortcut { pub fn view<'a>(&'a self, accounts: &'a [String]) -> Element<'a> { - row![ - widget::scrollable( + let open_folder = widget::button( + row![ + icons::folder_s(14), + widget::text!("Open {MENU_NAME} Folder").size(10) + ] + .align_y(Alignment::Center) + .spacing(10), + ) + .width(BWIDTH) + .on_press(Message::Shortcut(ShortcutMessage::OpenFolder)); + + widget::scrollable( + column![ + row![ + back_button().on_press(Message::MScreenOpen { + message: None, + clear_selection: false, + is_server: None + }), + widget::text("Create Launch Shortcut").size(20), + widget::horizontal_space(), + open_folder, + ] + .align_y(Alignment::Center) + .spacing(16), column![ - row![ - back_button().on_press(Message::MScreenOpen { - message: None, - clear_selection: false, - is_server: None - }), - widget::text("Create Launch Shortcut").size(20), - ] - .align_y(Alignment::Center) - .spacing(16), - column![ - widget::text!("Launch this instance directly from your Desktop or {MENU_NAME} without opening the launcher") - .size(14) - .style(tsubtitle), - widget::text("Note: You can manually pin this to Taskbar/Dock/Panel later").size(12).style(tsubtitle) - ].spacing(5), + widget::text!("Launch this instance directly from your Desktop/{MENU_NAME} with a single click") + .size(14) + .style(tsubtitle), + widget::text("Note: You can manually pin this to Taskbar/Dock/Panel later").size(12).style(tsubtitle) + ].spacing(5), - // Shortcut information (name, account, etc.) - self.get_info_fields(accounts), + // Shortcut information (name, account, etc.) + self.get_info_fields(accounts), - row![ + row![ + widget::container(column![ + widget::checkbox(format!("Add to {MENU_NAME}"), self.add_to_menu) + .on_toggle(|t| Message::Shortcut(ShortcutMessage::ToggleAddToMenu(t))) + .size(12) + .text_size(12), + widget::checkbox("Add to Desktop", self.add_to_desktop) + .on_toggle(|t| Message::Shortcut(ShortcutMessage::ToggleAddToDesktop(t))) + .size(12) + .text_size(12), + widget::Space::with_height(4), button_with_icon(icons::checkmark_s(14), "Create Shortcut", 14).on_press(Message::Shortcut(ShortcutMessage::SaveMenu)), - column![ - widget::checkbox(format!("Add to {MENU_NAME}"), self.add_to_menu) - .on_toggle(|t| Message::Shortcut(ShortcutMessage::ToggleAddToMenu(t))) - .size(12) - .text_size(12), - widget::checkbox("Add to Desktop", self.add_to_desktop) - .on_toggle(|t| Message::Shortcut(ShortcutMessage::ToggleAddToDesktop(t))) - .size(12) - .text_size(12), - ] - ] - .spacing(10), - column![ + ].spacing(1)).padding([11, 10]), + widget::container(column![ widget::text("Or save a shortcut file to use anywhere") .size(14) .style(tsubtitle), widget::text("(May not work everywhere)").size(12).style(tsubtitle), widget::Space::with_height(5), button_with_icon(icons::floppydisk_s(14), "Export Shortcut File...", 14).on_press(Message::Shortcut(ShortcutMessage::SaveCustom)), - ] - ] - .width(Length::Fill) - .padding(16) - .spacing(12) - ) - .style(|t: &LauncherTheme, s| t.style_scrollable_flat_dark(s)), - widget::scrollable( - column![ - widget::text("Existing Shortcuts").size(20), - widget::text( - "TODO: Store and show shortcut info here, and have options to delete them" - ) - .size(12) - ] - .padding(16) - .spacing(10) - ) - .height(Length::Fill) - .width(200) - .style(|t: &LauncherTheme, s| t.style_scrollable_flat_extra_dark(s)) - ] + ]).padding(10) + ].spacing(10) + ] + .width(Length::Fill) + .padding(16) + .spacing(12) + ) + .style(|t: &LauncherTheme, s| t.style_scrollable_flat_dark(s)) .into() } @@ -106,14 +106,14 @@ impl MenuShortcut { } column![ifield( - " Name:", + "Name:", widget::text_input("(Required)", &self.shortcut.name) .size(14) .on_input(|n| Message::Shortcut(ShortcutMessage::EditName(n))) )] .push_maybe(SHOW_DESC.then(|| { ifield( - " Description:", + "Description:", widget::text_input("Leave blank for none", &self.shortcut.description) .size(14) .on_input(|n| Message::Shortcut(ShortcutMessage::EditDescription(n))), @@ -137,5 +137,6 @@ impl MenuShortcut { .spacing(5), )) .spacing(5) + .padding([0, 1]) } } diff --git a/quantum_launcher/src/message_update/shortcuts.rs b/quantum_launcher/src/message_update/shortcuts.rs index 10f179fb2..d212528ad 100644 --- a/quantum_launcher/src/message_update/shortcuts.rs +++ b/quantum_launcher/src/message_update/shortcuts.rs @@ -20,6 +20,11 @@ impl Launcher { ShortcutMessage::Open => { self.shortcut_open(); } + ShortcutMessage::OpenFolder => { + if let Some(dir) = ezshortcut::get_menu_path() { + _ = open::that(&dir); + } + } ShortcutMessage::ToggleAddToMenu(t) => iflet!(menu, self.state, { if t || menu.add_to_desktop { menu.add_to_menu = t; diff --git a/quantum_launcher/src/state/message.rs b/quantum_launcher/src/state/message.rs index 933705e04..41dbce92b 100644 --- a/quantum_launcher/src/state/message.rs +++ b/quantum_launcher/src/state/message.rs @@ -356,6 +356,7 @@ pub enum GameLogMessage { #[derive(Debug, Clone)] pub enum ShortcutMessage { Open, + OpenFolder, ToggleAddToMenu(bool), ToggleAddToDesktop(bool), EditName(String), From a4f1828c65c47309481b29588a604fdfd07f1a32 Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Wed, 18 Feb 2026 20:42:30 +0530 Subject: [PATCH 19/30] Dev: Add extra handling for custom launcher dirs --- Cargo.lock | 1 + quantum_launcher/Cargo.toml | 1 + quantum_launcher/src/cli/mod.rs | 15 +++++++++++++-- quantum_launcher/src/main.rs | 6 +++--- .../src/message_update/shortcuts.rs | 17 ++++++++++++++++- 5 files changed, 34 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dded7a8a9..bba0e9204 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4125,6 +4125,7 @@ dependencies = [ "chrono", "clap", "dark-light", + "dirs", "embed-resource", "ezshortcut", "frostmark", diff --git a/quantum_launcher/Cargo.toml b/quantum_launcher/Cargo.toml index 4df8c2bf3..e2f7b3e8d 100644 --- a/quantum_launcher/Cargo.toml +++ b/quantum_launcher/Cargo.toml @@ -79,6 +79,7 @@ terminal_size = "0.4" notify = "8" open = "5.3.2" dark-light = "2" +dirs.workspace = true [target.'cfg(not(target_os = "macos"))'.dependencies] notify-rust = { version = "4", default-features = false, features = ["z-with-tokio"] } diff --git a/quantum_launcher/src/cli/mod.rs b/quantum_launcher/src/cli/mod.rs index 1571304e1..40f767ffe 100644 --- a/quantum_launcher/src/cli/mod.rs +++ b/quantum_launcher/src/cli/mod.rs @@ -1,4 +1,7 @@ -use std::sync::{LazyLock, RwLock}; +use std::{ + path::PathBuf, + sync::{LazyLock, RwLock}, +}; use clap::{Parser, Subcommand}; use owo_colors::{OwoColorize, Style}; @@ -36,6 +39,8 @@ struct Cli { #[arg(help = "Operate on servers, not instances")] #[arg(hide = true)] server: bool, + #[arg(long)] + dir: Option, } #[derive(Subcommand)] @@ -191,11 +196,17 @@ fn get_right_text() -> String { message } -pub fn start_cli(is_dir_err: bool) { +pub fn start_cli(is_dir_err: bool, launcher_dir: &mut Option) { let cli = Cli::parse(); *REDACT_SENSITIVE_INFO.lock().unwrap() = !cli.no_redact_info; *EXPERIMENTAL_SERVERS.write().unwrap() = cli.enable_server_manager; *EXPERIMENTAL_MMC_IMPORT.write().unwrap() = cli.enable_mmc_import; + + if let Some(p) = &cli.dir { + *launcher_dir = Some(p.clone()); + std::env::set_var("QLDIR", p); + } + if let Some(subcommand) = cli.command { if is_dir_err { std::process::exit(1); diff --git a/quantum_launcher/src/main.rs b/quantum_launcher/src/main.rs index 000d662b4..4b3d9ad50 100644 --- a/quantum_launcher/src/main.rs +++ b/quantum_launcher/src/main.rs @@ -164,8 +164,8 @@ fn main() { let is_new_user = file_utils::is_new_user(); // let is_new_user = true; // Uncomment to test the intro screen. - let (launcher_dir, is_dir_err) = load_launcher_dir(); - cli::start_cli(is_dir_err); + let (mut launcher_dir, is_dir_err) = load_launcher_dir(); + cli::start_cli(is_dir_err, &mut launcher_dir); info!(no_log, "Starting up the launcher... (OS: {OS_NAME})"); if let Some(dir) = &launcher_dir { @@ -327,7 +327,7 @@ fn do_migration() { } else if let Err(e) = file_utils::create_symlink(&new_dir, &legacy_dir) { eprintln!("Migration successful but couldn't create symlink to the legacy dir: {e}"); } else { - ql_core::info!("Migration successful!\nYour launcher files are now in ~./local/share/QuantumLauncher"); + println!("Migration successful!\nYour launcher files are now in ~./local/share/QuantumLauncher"); } } } diff --git a/quantum_launcher/src/message_update/shortcuts.rs b/quantum_launcher/src/message_update/shortcuts.rs index d212528ad..f3edf4352 100644 --- a/quantum_launcher/src/message_update/shortcuts.rs +++ b/quantum_launcher/src/message_update/shortcuts.rs @@ -1,6 +1,6 @@ use ezshortcut::Shortcut; use iced::Task; -use ql_core::{info, IntoStringError}; +use ql_core::{info, IntoStringError, LAUNCHER_DIR}; use crate::state::{ Launcher, MenuShortcut, Message, ShortcutMessage, State, NEW_ACCOUNT_NAME, OFFLINE_ACCOUNT_NAME, @@ -131,21 +131,36 @@ impl Launcher { .map_err(|n| format!("while getting path to current exe:\n{n}"))?; shortcut.exec = exec_path.to_string_lossy().to_string(); + + // Environment setup if instance.is_server() { shortcut .exec_args .push("--enable-server-manager".to_owned()); shortcut.exec_args.push("-s".to_owned()); } + if let Some(n) = dirs::data_dir().map(|d| d.join("QuantumLauncher")) { + if &*LAUNCHER_DIR != &n { + shortcut.exec_args.push("--dir".to_owned()); + shortcut + .exec_args + .push(LAUNCHER_DIR.to_string_lossy().to_string()); + } + } + + // Launch command shortcut .exec_args .extend(["launch".to_owned(), instance.get_name().to_owned()]); + + // Account setup if menu.account == OFFLINE_ACCOUNT_NAME { shortcut.exec_args.push(menu.account_offline.clone()); } else { shortcut.exec_args.push(menu.account.clone()); shortcut.exec_args.push("-u".to_owned()); } + shortcut.exec_args.push("--show-progress".to_owned()); Ok(shortcut) From 26b6d54ad63a78be067e7201d9056f6000232b2b Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Wed, 18 Feb 2026 20:42:43 +0530 Subject: [PATCH 20/30] Fix curseforge downloading never appearing to end --- quantum_launcher/src/message_update/mod.rs | 2 +- quantum_launcher/src/state/menu.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/quantum_launcher/src/message_update/mod.rs b/quantum_launcher/src/message_update/mod.rs index 0d7d6bbd8..573f2f4ea 100644 --- a/quantum_launcher/src/message_update/mod.rs +++ b/quantum_launcher/src/message_update/mod.rs @@ -379,7 +379,7 @@ impl Launcher { async move { ql_mod_manager::store::download_mod(&id, &selected_instance, None) .await - .map(|not_allowed| (ModId::Modrinth(project_id), not_allowed)) + .map(|not_allowed| (id, not_allowed)) }, |n| Message::InstallMods(InstallModsMessage::DownloadComplete(n.strerr())), ) diff --git a/quantum_launcher/src/state/menu.rs b/quantum_launcher/src/state/menu.rs index bae28f09d..a199c22c4 100644 --- a/quantum_launcher/src/state/menu.rs +++ b/quantum_launcher/src/state/menu.rs @@ -406,7 +406,7 @@ pub struct MenuLauncherUpdate { pub progress: Option>, } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub enum ModOperation { Downloading, Deleting, From 18d081e94147bb8ed26b5f71c8981235c3bc4e95 Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Sat, 21 Feb 2026 20:16:56 +0530 Subject: [PATCH 21/30] Dev: apply PR nits, clean up UI code --- Cargo.lock | 7 + crates/ezshortcut/src/os/macos.rs | 19 ++- crates/ezshortcut/src/os/unix.rs | 6 +- crates/ezshortcut/src/os/windows.rs | 32 ++-- quantum_launcher/Cargo.toml | 1 + .../src/menu_renderer/shortcuts.rs | 154 ++++++++++++------ 6 files changed, 150 insertions(+), 69 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bba0e9204..f83ded0d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -892,6 +892,12 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "constcat" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d3e02915a2cea4d74caa8681e2d44b1c3254bdbf17d11d41d587ff858832c" + [[package]] name = "core-foundation" version = "0.9.4" @@ -4124,6 +4130,7 @@ dependencies = [ "cfg-if", "chrono", "clap", + "constcat", "dark-light", "dirs", "embed-resource", diff --git a/crates/ezshortcut/src/os/macos.rs b/crates/ezshortcut/src/os/macos.rs index b6f8c7d84..fdb97b7f6 100644 --- a/crates/ezshortcut/src/os/macos.rs +++ b/crates/ezshortcut/src/os/macos.rs @@ -55,7 +55,7 @@ exec {:?} {} CFBundleExecutable ql_shortcut CFBundleIdentifier - io.github.Mrmayman.QLShortcut_{} + io.github.Mrmayman.QLShortcut_{sanitized} CFBundleInfoDictionaryVersion 6.0 CFBundleName @@ -71,8 +71,8 @@ exec {:?} {} "#, - shortcut.name, - make_filename_safe(&shortcut.name, true), + sanitized = xml_escape(&make_filename_safe(&shortcut.name, true)), + xml_escape(&shortcut.name), ); // TODO: Before the you could have // ``` @@ -97,3 +97,16 @@ pub async fn create_in_applications(shortcut: &Shortcut) -> std::io::Result<()> refresh_applications(); Ok(()) } + +fn xml_escape(s: &str) -> String { + s.chars() + .map(|c| match c { + '&' => "&".into(), + '<' => "<".into(), + '>' => ">".into(), + '"' => """.into(), + '\'' => "'".into(), + _ => c.to_string(), + }) + .collect() +} diff --git a/crates/ezshortcut/src/os/unix.rs b/crates/ezshortcut/src/os/unix.rs index 665e4f5ce..abb2e2dbc 100644 --- a/crates/ezshortcut/src/os/unix.rs +++ b/crates/ezshortcut/src/os/unix.rs @@ -69,10 +69,8 @@ Categories=Game;", ); match fs::metadata(path).await { - Ok(n) => { - if n.is_dir() { - write_file(&path.join(shortcut.get_filename()), content).await?; - } + Ok(n) if n.is_dir() => { + write_file(&path.join(shortcut.get_filename()), content).await?; } _ => write_file(path, content).await?, }; diff --git a/crates/ezshortcut/src/os/windows.rs b/crates/ezshortcut/src/os/windows.rs index eac34a3ca..a798c85d1 100644 --- a/crates/ezshortcut/src/os/windows.rs +++ b/crates/ezshortcut/src/os/windows.rs @@ -1,4 +1,4 @@ -use tokio::fs; +use tokio::{fs, task::spawn_blocking}; use windows::{ core::{Interface, HSTRING}, Win32::{ @@ -13,9 +13,9 @@ use windows::{ }; use crate::Shortcut; -use std::path::Path; +use std::path::{Path, PathBuf}; -pub fn get_menu_path() -> Option { +pub fn get_menu_path() -> Option { unsafe { let path_ptr = SHGetKnownFolderPath(&FOLDERID_Programs, KNOWN_FOLDER_FLAG::default(), None).ok()?; @@ -27,16 +27,16 @@ pub fn get_menu_path() -> Option { } pub async fn create(shortcut: &Shortcut, path: impl AsRef) -> std::io::Result<()> { - create_inner(shortcut, path.as_ref()).await?; - Ok(()) -} - -async fn create_inner(shortcut: &Shortcut, path: &Path) -> std::io::Result<()> { - let path = match fs::metadata(path).await { + let path = match fs::metadata(path.as_ref()).await { Ok(n) if n.is_dir() => path.join(shortcut.get_filename()), _ => path.to_owned(), }; + spawn_blocking(move || create_inner(shortcut, path)).await??; + Ok(()) +} + +fn create_inner(shortcut: &Shortcut, path: PathBuf) -> std::io::Result<()> { let args = shortcut .exec_args .iter() @@ -72,13 +72,15 @@ fn ioerr(err: impl std::error::Error + Send + Sync + 'static) -> std::io::Error } pub async fn create_in_applications(shortcut: &Shortcut) -> std::io::Result<()> { - let start_menu = tokio::task::spawn_blocking(|| get_menu_path()) - .await? - .ok_or_else(|| { - std::io::Error::new(std::io::ErrorKind::NotFound, "Start menu path not found") - })?; + let start_menu = spawn_blocking(|| get_menu_path()).await?.ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::NotFound, "Start menu path not found") + })?; fs::create_dir_all(&start_menu).await?; - create_inner(shortcut, &start_menu).await + + let file_path = start_menu.join(shortcut.get_filename()); + spawn_blocking(move || create_inner(shortcut, file_path)).await??; + + Ok(()) } fn quote_windows_arg(arg: &str) -> String { diff --git a/quantum_launcher/Cargo.toml b/quantum_launcher/Cargo.toml index e2f7b3e8d..26b7620e0 100644 --- a/quantum_launcher/Cargo.toml +++ b/quantum_launcher/Cargo.toml @@ -80,6 +80,7 @@ notify = "8" open = "5.3.2" dark-light = "2" dirs.workspace = true +constcat = "0.6" [target.'cfg(not(target_os = "macos"))'.dependencies] notify-rust = { version = "4", default-features = false, features = ["z-with-tokio"] } diff --git a/quantum_launcher/src/menu_renderer/shortcuts.rs b/quantum_launcher/src/menu_renderer/shortcuts.rs index aff1eeb0d..cb58808d8 100644 --- a/quantum_launcher/src/menu_renderer/shortcuts.rs +++ b/quantum_launcher/src/menu_renderer/shortcuts.rs @@ -1,6 +1,6 @@ use crate::{ icons, - menu_renderer::{back_button, button_with_icon, tsubtitle, Element}, + menu_renderer::{back_button, button_with_icon, tooltip, tsubtitle, Element}, state::{MenuShortcut, Message, ShortcutMessage, OFFLINE_ACCOUNT_NAME}, stylesheet::styles::LauncherTheme, }; @@ -13,29 +13,22 @@ use iced::{ cfg_if!(if #[cfg(target_os = "windows")] { const MENU_NAME: &str = "the Start Menu"; const SHOW_DESC: bool = true; - const BWIDTH: u16 = 120; + const FOLDER_BUTTON_WIDTH: u16 = 120; } else if #[cfg(target_os = "macos")] { const MENU_NAME: &str = "Applications"; - const SHOW_DESC: bool = false; - const BWIDTH: u16 = 140; + const SHOW_DESC: bool = false; // Shortcut description is unsupported on macOS + const FOLDER_BUTTON_WIDTH: u16 = 140; } else { const MENU_NAME: &str = "the Applications Menu"; const SHOW_DESC: bool = true; - const BWIDTH: u16 = 150; + const FOLDER_BUTTON_WIDTH: u16 = 150; }); +const ACTION_BUTTON_MENU: &str = constcat::concat!("Add to ", MENU_NAME); +const FOLDER_BUTTON: &str = constcat::concat!("Open ", MENU_NAME, " Folder"); impl MenuShortcut { pub fn view<'a>(&'a self, accounts: &'a [String]) -> Element<'a> { - let open_folder = widget::button( - row![ - icons::folder_s(14), - widget::text!("Open {MENU_NAME} Folder").size(10) - ] - .align_y(Alignment::Center) - .spacing(10), - ) - .width(BWIDTH) - .on_press(Message::Shortcut(ShortcutMessage::OpenFolder)); + let action_buttons = self.get_action_buttons(); widget::scrollable( column![ @@ -47,51 +40,108 @@ impl MenuShortcut { }), widget::text("Create Launch Shortcut").size(20), widget::horizontal_space(), - open_folder, + open_folder_button(), ] .align_y(Alignment::Center) .spacing(16), column![ - widget::text!("Launch this instance directly from your Desktop/{MENU_NAME} with a single click") - .size(14) - .style(tsubtitle), - widget::text("Note: You can manually pin this to Taskbar/Dock/Panel later").size(12).style(tsubtitle) - ].spacing(5), - + widget::text!( + "Launch the instance directly from {MENU_NAME}/Desktop, with a single click" + ) + .size(14) + .style(tsubtitle), + widget::text("Note: You can manually pin this to Taskbar/Dock/Panel later") + .size(12) + .style(tsubtitle) + ] + .spacing(5), // Shortcut information (name, account, etc.) self.get_info_fields(accounts), - - row![ - widget::container(column![ - widget::checkbox(format!("Add to {MENU_NAME}"), self.add_to_menu) - .on_toggle(|t| Message::Shortcut(ShortcutMessage::ToggleAddToMenu(t))) - .size(12) - .text_size(12), - widget::checkbox("Add to Desktop", self.add_to_desktop) - .on_toggle(|t| Message::Shortcut(ShortcutMessage::ToggleAddToDesktop(t))) - .size(12) - .text_size(12), - widget::Space::with_height(4), - button_with_icon(icons::checkmark_s(14), "Create Shortcut", 14).on_press(Message::Shortcut(ShortcutMessage::SaveMenu)), - ].spacing(1)).padding([11, 10]), - widget::container(column![ - widget::text("Or save a shortcut file to use anywhere") - .size(14) - .style(tsubtitle), - widget::text("(May not work everywhere)").size(12).style(tsubtitle), - widget::Space::with_height(5), - button_with_icon(icons::floppydisk_s(14), "Export Shortcut File...", 14).on_press(Message::Shortcut(ShortcutMessage::SaveCustom)), - ]).padding(10) - ].spacing(10) + action_buttons, ] .width(Length::Fill) .padding(16) - .spacing(12) + .spacing(12), ) .style(|t: &LauncherTheme, s| t.style_scrollable_flat_dark(s)) .into() } + fn get_action_buttons(&self) -> widget::Row<'_, Message, LauncherTheme> { + fn tooltip_maybe<'a>(t: Option<&'a str>, e: impl Into>) -> Element<'a> { + if let Some(t) = t { + tooltip(e, t, widget::tooltip::Position::FollowCursor).into() + } else { + e.into() + } + } + + let disabled_tooltip = self.get_disabled_tooltip(); + + row![ + widget::container( + column![ + widget::checkbox(ACTION_BUTTON_MENU, self.add_to_menu) + .on_toggle(|t| Message::Shortcut(ShortcutMessage::ToggleAddToMenu(t))) + .size(12) + .text_size(12), + widget::checkbox("Add to Desktop", self.add_to_desktop) + .on_toggle(|t| Message::Shortcut(ShortcutMessage::ToggleAddToDesktop(t))) + .size(12) + .text_size(12), + widget::Space::with_height(4), + tooltip_maybe( + disabled_tooltip, + button_with_icon(icons::checkmark_s(14), "Create Shortcut", 14) + .on_press_maybe( + disabled_tooltip + .is_none() + .then_some(Message::Shortcut(ShortcutMessage::SaveMenu)) + ) + ), + ] + .spacing(1) + ) + .padding([10, 10]), + widget::container(column![ + widget::text("Or save a shortcut file to use anywhere") + .size(14) + .style(tsubtitle), + widget::text("(May not work everywhere)") + .size(12) + .style(tsubtitle), + widget::Space::with_height(5), + tooltip_maybe( + disabled_tooltip, + button_with_icon(icons::floppydisk_s(14), "Export Shortcut File...", 14) + .on_press_maybe( + disabled_tooltip + .is_none() + .then_some(Message::Shortcut(ShortcutMessage::SaveCustom)) + ) + ), + ]) + .padding(10) + ] + .spacing(10) + } + + fn get_disabled_tooltip(&self) -> Option<&'static str> { + if self.shortcut.name.is_empty() { + Some("Shortcut name is empty") + } else if self.account == OFFLINE_ACCOUNT_NAME { + if self.account_offline.trim().is_empty() { + Some("Username is empty") + } else if self.account_offline.contains(' ') { + Some("Username has spaces") + } else { + None + } + } else { + None + } + } + fn get_info_fields<'a>( &'a self, accounts: &'a [String], @@ -120,7 +170,7 @@ impl MenuShortcut { ) })) .push(ifield( - " Account:", + "Account:", row![widget::pick_list(accounts, Some(&self.account), |n| { Message::Shortcut(ShortcutMessage::AccountSelected(n)) }) @@ -140,3 +190,13 @@ impl MenuShortcut { .padding([0, 1]) } } + +fn open_folder_button() -> widget::Button<'static, Message, LauncherTheme> { + widget::button( + row![icons::folder_s(14), widget::text(FOLDER_BUTTON).size(10)] + .align_y(Alignment::Center) + .spacing(10), + ) + .width(FOLDER_BUTTON_WIDTH) + .on_press(Message::Shortcut(ShortcutMessage::OpenFolder)) +} From 460d9373e5fa15819502f612f1d7ea83dea4851f Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Sat, 21 Feb 2026 20:33:02 +0530 Subject: [PATCH 22/30] Fix build errors (1) --- crates/ezshortcut/src/os/macos.rs | 4 ++-- crates/ezshortcut/src/os/windows.rs | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/ezshortcut/src/os/macos.rs b/crates/ezshortcut/src/os/macos.rs index fdb97b7f6..d37293e8b 100644 --- a/crates/ezshortcut/src/os/macos.rs +++ b/crates/ezshortcut/src/os/macos.rs @@ -59,7 +59,7 @@ exec {:?} {} CFBundleInfoDictionaryVersion 6.0 CFBundleName - {} + {display_name} CFBundlePackageType APPL CFBundleShortVersionString @@ -71,8 +71,8 @@ exec {:?} {} "#, + display_name = xml_escape(&shortcut.name), sanitized = xml_escape(&make_filename_safe(&shortcut.name, true)), - xml_escape(&shortcut.name), ); // TODO: Before the you could have // ``` diff --git a/crates/ezshortcut/src/os/windows.rs b/crates/ezshortcut/src/os/windows.rs index a798c85d1..5d50e83fa 100644 --- a/crates/ezshortcut/src/os/windows.rs +++ b/crates/ezshortcut/src/os/windows.rs @@ -27,7 +27,8 @@ pub fn get_menu_path() -> Option { } pub async fn create(shortcut: &Shortcut, path: impl AsRef) -> std::io::Result<()> { - let path = match fs::metadata(path.as_ref()).await { + let path = path.as_ref(); + let path = match fs::metadata(path).await { Ok(n) if n.is_dir() => path.join(shortcut.get_filename()), _ => path.to_owned(), }; @@ -78,7 +79,8 @@ pub async fn create_in_applications(shortcut: &Shortcut) -> std::io::Result<()> fs::create_dir_all(&start_menu).await?; let file_path = start_menu.join(shortcut.get_filename()); - spawn_blocking(move || create_inner(shortcut, file_path)).await??; + let shortcut = shortcut.clone(); + spawn_blocking(move || create_inner(&shortcut, file_path)).await??; Ok(()) } From 1628024d5d8c54848192f6a00aae0cb88843765e Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Sat, 21 Feb 2026 21:16:02 +0530 Subject: [PATCH 23/30] Fix build errors (2) --- crates/ezshortcut/src/os/windows.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/ezshortcut/src/os/windows.rs b/crates/ezshortcut/src/os/windows.rs index 5d50e83fa..2b6a45767 100644 --- a/crates/ezshortcut/src/os/windows.rs +++ b/crates/ezshortcut/src/os/windows.rs @@ -33,7 +33,8 @@ pub async fn create(shortcut: &Shortcut, path: impl AsRef) -> std::io::Res _ => path.to_owned(), }; - spawn_blocking(move || create_inner(shortcut, path)).await??; + let shortcut = shortcut.clone(); + spawn_blocking(move || create_inner(&shortcut, path)).await??; Ok(()) } From 1b558a3c865ceaecac113390da161254a626a713 Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Sat, 21 Feb 2026 23:34:37 +0530 Subject: [PATCH 24/30] Fix PR review issues --- crates/ezshortcut/src/lib.rs | 2 +- crates/ezshortcut/src/os/macos.rs | 2 +- crates/ezshortcut/src/os/mod.rs | 4 ++++ crates/ezshortcut/src/os/unix.rs | 3 ++- quantum_launcher/src/cli/mod.rs | 2 +- quantum_launcher/src/message_update/shortcuts.rs | 2 +- 6 files changed, 10 insertions(+), 5 deletions(-) diff --git a/crates/ezshortcut/src/lib.rs b/crates/ezshortcut/src/lib.rs index 9482dbe08..36a610de3 100644 --- a/crates/ezshortcut/src/lib.rs +++ b/crates/ezshortcut/src/lib.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; mod os; -pub use os::{get_menu_path, EXTENSION}; +pub use os::{get_menu_path, EXTENSION, EXTENSION_S}; pub fn get_desktop_dir() -> Option { dirs::desktop_dir().or_else(|| dirs::home_dir().map(|n| n.join("Desktop"))) diff --git a/crates/ezshortcut/src/os/macos.rs b/crates/ezshortcut/src/os/macos.rs index d37293e8b..f943d0976 100644 --- a/crates/ezshortcut/src/os/macos.rs +++ b/crates/ezshortcut/src/os/macos.rs @@ -19,7 +19,7 @@ pub async fn create(shortcut: &Shortcut, path: impl AsRef) -> std::io::Res } async fn create_inner(shortcut: &Shortcut, path: &Path) -> std::io::Result { - let path = if path.extension().is_some_and(|n| n == ".app") { + let path = if path.extension().is_some_and(|n| n == "app") { path.to_owned() } else { path.join(shortcut.get_filename()) diff --git a/crates/ezshortcut/src/os/mod.rs b/crates/ezshortcut/src/os/mod.rs index bf7e23dc1..88c036f27 100644 --- a/crates/ezshortcut/src/os/mod.rs +++ b/crates/ezshortcut/src/os/mod.rs @@ -5,17 +5,21 @@ cfg_if!( mod windows; pub use windows::*; pub const EXTENSION: &str = ".lnk"; + pub const EXTENSION_S: &str = "lnk"; } else if #[cfg(target_os = "macos")] { mod macos; pub use macos::*; pub const EXTENSION: &str = ".app"; + pub const EXTENSION_S: &str = "app"; } else if #[cfg(target_family = "unix")] { mod unix; pub use unix::*; pub const EXTENSION: &str = ".desktop"; + pub const EXTENSION_S: &str = "desktop"; } else { mod stub; pub use stub::*; pub const EXTENSION: &str = ""; + pub const EXTENSION_S: &str = ""; } ); diff --git a/crates/ezshortcut/src/os/unix.rs b/crates/ezshortcut/src/os/unix.rs index abb2e2dbc..32bdaf381 100644 --- a/crates/ezshortcut/src/os/unix.rs +++ b/crates/ezshortcut/src/os/unix.rs @@ -31,6 +31,7 @@ pub async fn create_in_applications(shortcut: &Shortcut) -> std::io::Result<()> "Couldn't access data dir (.local/share)", ))? .join("applications"); + fs::create_dir_all(&path).await?; create(shortcut, path).await?; refresh_applications(); Ok(()) @@ -51,7 +52,7 @@ async fn create_inner(shortcut: &Shortcut, path: &Path) -> Result<(), std::io::E Version=1.0 Type=Application Name={name} -{icon}{description}Exec={exec} {args} +{icon}{description}Exec={exec:?} {args} Terminal=false Categories=Game;", name = shortcut.name, diff --git a/quantum_launcher/src/cli/mod.rs b/quantum_launcher/src/cli/mod.rs index 40f767ffe..b06408154 100644 --- a/quantum_launcher/src/cli/mod.rs +++ b/quantum_launcher/src/cli/mod.rs @@ -208,7 +208,7 @@ pub fn start_cli(is_dir_err: bool, launcher_dir: &mut Option) { } if let Some(subcommand) = cli.command { - if is_dir_err { + if is_dir_err && cli.dir.is_none() { std::process::exit(1); } let runtime = tokio::runtime::Runtime::new().unwrap(); diff --git a/quantum_launcher/src/message_update/shortcuts.rs b/quantum_launcher/src/message_update/shortcuts.rs index f3edf4352..4b4c55d18 100644 --- a/quantum_launcher/src/message_update/shortcuts.rs +++ b/quantum_launcher/src/message_update/shortcuts.rs @@ -55,7 +55,7 @@ impl Launcher { ShortcutMessage::SaveCustom => iflet!(menu, self.state, { return Ok(Task::perform( rfd::AsyncFileDialog::new() - .add_filter("Shortcut", &[ezshortcut::EXTENSION]) + .add_filter("Shortcut", &[ezshortcut::EXTENSION_S]) .set_file_name(menu.shortcut.get_filename()) .set_title("Save shortcut to...") .save_file(), From 151cd246f30c567b7c5f98ea0ac6b8769bfd9551 Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Sat, 21 Feb 2026 23:39:42 +0530 Subject: [PATCH 25/30] Fix clippy lints --- quantum_launcher/src/message_update/shortcuts.rs | 2 +- quantum_launcher/src/tick.rs | 1 + quantum_launcher/src/update.rs | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/quantum_launcher/src/message_update/shortcuts.rs b/quantum_launcher/src/message_update/shortcuts.rs index 4b4c55d18..f86b89a71 100644 --- a/quantum_launcher/src/message_update/shortcuts.rs +++ b/quantum_launcher/src/message_update/shortcuts.rs @@ -140,7 +140,7 @@ impl Launcher { shortcut.exec_args.push("-s".to_owned()); } if let Some(n) = dirs::data_dir().map(|d| d.join("QuantumLauncher")) { - if &*LAUNCHER_DIR != &n { + if *LAUNCHER_DIR != n { shortcut.exec_args.push("--dir".to_owned()); shortcut .exec_args diff --git a/quantum_launcher/src/tick.rs b/quantum_launcher/src/tick.rs index 02af5d9d7..0bd23f919 100644 --- a/quantum_launcher/src/tick.rs +++ b/quantum_launcher/src/tick.rs @@ -170,6 +170,7 @@ impl Launcher { Task::none() } + #[allow(clippy::manual_is_multiple_of)] // Maintain Rust MSRV pub fn autosave_config(&mut self) -> Task { if self.tick_timer % 5 == 0 && self.autosave.insert(AutoSaveKind::LauncherConfig) { let launcher_config = self.config.clone(); diff --git a/quantum_launcher/src/update.rs b/quantum_launcher/src/update.rs index 9215ee6fa..2e2ab0b09 100644 --- a/quantum_launcher/src/update.rs +++ b/quantum_launcher/src/update.rs @@ -465,6 +465,7 @@ impl Launcher { .config .ui_mode .is_none_or(|n| n == LauncherThemeLightness::Auto); + #[allow(clippy::manual_is_multiple_of)] // Maintain Rust MSRV let interval = self.tick_timer % INTERVAL == 0; if is_auto_theme && interval { From 4d877e47fa5d927cb689e1fe07ea6b082db59edd Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Sun, 22 Feb 2026 00:22:44 +0530 Subject: [PATCH 26/30] Fix many more clippy lints (2) --- crates/ezshortcut/src/lib.rs | 6 ++- crates/ezshortcut/src/os/macos.rs | 1 + crates/ezshortcut/src/os/stub.rs | 1 + crates/ezshortcut/src/os/unix.rs | 1 + crates/ezshortcut/src/os/windows.rs | 5 +- crates/ql_core/src/print/mod.rs | 4 +- crates/ql_core/src/structs.rs | 1 + crates/ql_instances/src/download/libraries.rs | 4 +- crates/ql_java_handler/src/alternate_java.rs | 2 +- .../ql_mod_manager/src/loaders/forge/mod.rs | 3 +- crates/ql_mod_manager/src/loaders/neoforge.rs | 2 +- crates/ql_mod_manager/src/loaders/paper.rs | 2 +- crates/ql_mod_manager/src/store/mod.rs | 1 + crates/ql_packager/src/multimc.rs | 18 ++++--- quantum_launcher/src/cli/command.rs | 10 ++-- quantum_launcher/src/cli/mod.rs | 2 +- quantum_launcher/src/config.rs | 8 ++- quantum_launcher/src/menu_renderer/create.rs | 5 +- .../src/menu_renderer/edit_instance.rs | 13 +++-- quantum_launcher/src/menu_renderer/mod.rs | 2 +- .../src/message_handler/iced_event.rs | 16 +++--- quantum_launcher/src/message_handler/mod.rs | 4 +- .../src/message_update/accounts.rs | 2 +- .../src/message_update/create_instance.rs | 9 ++-- .../src/message_update/edit_instance.rs | 54 +++++++++---------- .../src/message_update/manage_mods.rs | 4 +- .../src/message_update/shortcuts.rs | 4 +- quantum_launcher/src/state/mod.rs | 2 +- quantum_launcher/src/stylesheet/widgets.rs | 23 ++++---- quantum_launcher/src/update.rs | 2 +- 30 files changed, 105 insertions(+), 106 deletions(-) diff --git a/crates/ezshortcut/src/lib.rs b/crates/ezshortcut/src/lib.rs index 36a610de3..5da84c8a1 100644 --- a/crates/ezshortcut/src/lib.rs +++ b/crates/ezshortcut/src/lib.rs @@ -3,6 +3,7 @@ use std::path::{Path, PathBuf}; mod os; pub use os::{get_menu_path, EXTENSION, EXTENSION_S}; +#[must_use] pub fn get_desktop_dir() -> Option { dirs::desktop_dir().or_else(|| dirs::home_dir().map(|n| n.join("Desktop"))) } @@ -35,6 +36,7 @@ impl Shortcut { } /// Gets the recommended filename for the shortcut based on OS behavior. + #[must_use] pub fn get_filename(&self) -> String { let mut filtered_name = make_filename_safe(&self.name, !cfg!(target_os = "windows")); filtered_name.push_str(EXTENSION); @@ -58,8 +60,8 @@ fn make_filename_safe(input: &str, remove_spaces: bool) -> String { match c { '-' | '_' | '.' => out.push(c), ' ' | '/' | '\\' | '|' | ':' => out.push('_'), - '<' | '>' | '"' | '\'' | '?' | '*' => continue, - c if c.is_control() => continue, + '<' | '>' | '"' | '\'' | '?' | '*' => {} + c if c.is_control() => {} c if c.is_whitespace() => out.push(if remove_spaces { '_' } else { ' ' }), _ => out.push(c), diff --git a/crates/ezshortcut/src/os/macos.rs b/crates/ezshortcut/src/os/macos.rs index f943d0976..e9cc37964 100644 --- a/crates/ezshortcut/src/os/macos.rs +++ b/crates/ezshortcut/src/os/macos.rs @@ -3,6 +3,7 @@ use tokio::{fs, process::Command}; use crate::{make_filename_safe, Shortcut}; use std::path::{Path, PathBuf}; +#[must_use] pub fn get_menu_path() -> Option { dirs::home_dir().map(|h| h.join("Applications")) } diff --git a/crates/ezshortcut/src/os/stub.rs b/crates/ezshortcut/src/os/stub.rs index eb20fb7ab..3bd764d8a 100644 --- a/crates/ezshortcut/src/os/stub.rs +++ b/crates/ezshortcut/src/os/stub.rs @@ -1,6 +1,7 @@ use crate::Shortcut; use std::path::{Path, PathBuf}; +#[must_use] pub fn get_menu_path() -> Option { None } diff --git a/crates/ezshortcut/src/os/unix.rs b/crates/ezshortcut/src/os/unix.rs index 32bdaf381..e465945a3 100644 --- a/crates/ezshortcut/src/os/unix.rs +++ b/crates/ezshortcut/src/os/unix.rs @@ -7,6 +7,7 @@ use tokio::fs; use crate::Shortcut; +#[must_use] pub fn get_menu_path() -> Option { if let Ok(dir) = std::env::var("XDG_DATA_HOME") { Some(PathBuf::from(dir).join("applications")) diff --git a/crates/ezshortcut/src/os/windows.rs b/crates/ezshortcut/src/os/windows.rs index 2b6a45767..eaeb105e6 100644 --- a/crates/ezshortcut/src/os/windows.rs +++ b/crates/ezshortcut/src/os/windows.rs @@ -15,6 +15,7 @@ use windows::{ use crate::Shortcut; use std::path::{Path, PathBuf}; +#[must_use] pub fn get_menu_path() -> Option { unsafe { let path_ptr = @@ -70,11 +71,11 @@ fn create_inner(shortcut: &Shortcut, path: PathBuf) -> std::io::Result<()> { } fn ioerr(err: impl std::error::Error + Send + Sync + 'static) -> std::io::Error { - std::io::Error::new(std::io::ErrorKind::Other, Box::new(err)) + std::io::Error::other(Box::new(err)) } pub async fn create_in_applications(shortcut: &Shortcut) -> std::io::Result<()> { - let start_menu = spawn_blocking(|| get_menu_path()).await?.ok_or_else(|| { + let start_menu = spawn_blocking(get_menu_path).await?.ok_or_else(|| { std::io::Error::new(std::io::ErrorKind::NotFound, "Start menu path not found") })?; fs::create_dir_all(&start_menu).await?; diff --git a/crates/ql_core/src/print/mod.rs b/crates/ql_core/src/print/mod.rs index 5f6659bcd..7e1cd198f 100644 --- a/crates/ql_core/src/print/mod.rs +++ b/crates/ql_core/src/print/mod.rs @@ -22,8 +22,8 @@ pub static REDACTION_USERNAME: LazyLock<(Vec, String)> = LazyLock::new(| return ( vec![ home_str.to_owned(), - home_str.replace("\\", "/"), - home_str.replace("\\", "\\\\"), + home_str.replace('\\', "/"), + home_str.replace('\\', "\\\\"), ], username.to_owned(), ); diff --git a/crates/ql_core/src/structs.rs b/crates/ql_core/src/structs.rs index 32f61bc67..493768ae0 100644 --- a/crates/ql_core/src/structs.rs +++ b/crates/ql_core/src/structs.rs @@ -62,6 +62,7 @@ impl Loader { Self::Rift, ]; + #[must_use] pub fn not_vanilla(self) -> Option { (!self.is_vanilla()).then_some(self) } diff --git a/crates/ql_instances/src/download/libraries.rs b/crates/ql_instances/src/download/libraries.rs index ab5a9ddc7..fc39625cd 100644 --- a/crates/ql_instances/src/download/libraries.rs +++ b/crates/ql_instances/src/download/libraries.rs @@ -304,10 +304,10 @@ impl GameDownloader { async fn extract_file(&self, mut url: String) -> Result<(), DownloadError> { if url == "https://github.com/theofficialgman/lwjgl3-binaries-arm64/raw/lwjgl-3.1.6/lwjgl-jemalloc-natives-linux.jar" { - url = "https://github.com/theofficialgman/lwjgl3-binaries-arm64/raw/lwjgl-3.1.6/lwjgl-jemalloc-patched-natives-linux-arm64.jar".to_owned(); + "https://github.com/theofficialgman/lwjgl3-binaries-arm64/raw/lwjgl-3.1.6/lwjgl-jemalloc-patched-natives-linux-arm64.jar".clone_into(&mut url); } if (cfg!(target_arch = "aarch64") && url == MACOS_ARM_LWJGL_294_1) || url == "https://github.com/MinecraftMachina/lwjgl/releases/download/2.9.4-20150209-mmachina.2/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar" { - url = MACOS_ARM_LWJGL_294_2.to_owned(); + MACOS_ARM_LWJGL_294_2.clone_into(&mut url); } #[cfg(any( diff --git a/crates/ql_java_handler/src/alternate_java.rs b/crates/ql_java_handler/src/alternate_java.rs index d116e85ef..b728d0640 100644 --- a/crates/ql_java_handler/src/alternate_java.rs +++ b/crates/ql_java_handler/src/alternate_java.rs @@ -34,7 +34,7 @@ pub(crate) async fn install( } else if url.ends_with("zip") { file_utils::extract_zip_archive(Cursor::new(file_bytes), &install_dir, true).await?; } else { - return Err(JavaInstallError::UnknownExtension(url.to_owned())); + return Err(JavaInstallError::UnknownExtension(url)); } Ok(()) } diff --git a/crates/ql_mod_manager/src/loaders/forge/mod.rs b/crates/ql_mod_manager/src/loaders/forge/mod.rs index 4cc78b05f..ca2e2a82b 100644 --- a/crates/ql_mod_manager/src/loaders/forge/mod.rs +++ b/crates/ql_mod_manager/src/loaders/forge/mod.rs @@ -229,8 +229,7 @@ impl ForgeInstaller { self.version_json .javaVersion .clone() - .map(JavaVersion::from) - .unwrap_or(JavaVersion::Java21) + .map_or(JavaVersion::Java21, JavaVersion::from) } else { JavaVersion::Java8 }; diff --git a/crates/ql_mod_manager/src/loaders/neoforge.rs b/crates/ql_mod_manager/src/loaders/neoforge.rs index 67a6869bd..315715e39 100644 --- a/crates/ql_mod_manager/src/loaders/neoforge.rs +++ b/crates/ql_mod_manager/src/loaders/neoforge.rs @@ -332,7 +332,7 @@ pub async fn run_installer( .current_dir(if is_server { neoforge_dir .parent() - .map_or(neoforge_dir.join(".."), |n| n.to_owned()) + .map_or(neoforge_dir.join(".."), Path::to_owned) } else { neoforge_dir.to_owned() }); diff --git a/crates/ql_mod_manager/src/loaders/paper.rs b/crates/ql_mod_manager/src/loaders/paper.rs index 7309bd75b..dda452fa1 100644 --- a/crates/ql_mod_manager/src/loaders/paper.rs +++ b/crates/ql_mod_manager/src/loaders/paper.rs @@ -42,7 +42,7 @@ impl PaperVer { pub async fn get(&self, version: &str) -> Result { if let PaperVer::Full(n) = self { return Ok(n.clone()); - }; + } let list = get_list_of_versions(version.to_owned()).await?; Ok(match self { diff --git a/crates/ql_mod_manager/src/store/mod.rs b/crates/ql_mod_manager/src/store/mod.rs index 8f584b771..b16b4c2a7 100644 --- a/crates/ql_mod_manager/src/store/mod.rs +++ b/crates/ql_mod_manager/src/store/mod.rs @@ -273,6 +273,7 @@ pub struct SearchMod { } impl SearchMod { + #[must_use] pub fn get_id(&self, backend: StoreBackendType) -> ModId { ModId::from_pair(&self.id, backend) } diff --git a/crates/ql_packager/src/multimc.rs b/crates/ql_packager/src/multimc.rs index 85d748eef..28210e8b8 100644 --- a/crates/ql_packager/src/multimc.rs +++ b/crates/ql_packager/src/multimc.rs @@ -88,7 +88,7 @@ pub async fn import( ) .await?; - install_loader(&sender, &instance, &instance_recipe).await?; + install_loader(sender.as_deref(), &instance, &instance_recipe).await?; copy_files(temp_dir, sender, &instance).await?; @@ -270,7 +270,7 @@ async fn get_instance_recipe(mmc_pack: &MmcPack) -> Result>>, + sender: Option<&Sender>, instance: &InstanceSelection, instance_recipe: &InstanceRecipe, ) -> Result<(), InstancePackageError> { @@ -278,7 +278,7 @@ async fn install_loader( match loader { n @ (Loader::Fabric | Loader::Quilt) => { install_fabric( - sender.as_deref(), + sender, instance, instance_recipe.loader_version.clone(), matches!(n, Loader::Quilt), @@ -287,7 +287,7 @@ async fn install_loader( } n @ (Loader::Forge | Loader::Neoforge) => { mmc_forge( - sender.clone(), + sender, instance, instance_recipe.loader_version.clone(), matches!(n, Loader::Neoforge), @@ -341,8 +341,10 @@ async fn install_fabric( get_list_of_versions_from_backend("1.14.4", backend, false) .await? .first() - .map(|n| n.loader.version.clone()) - .unwrap_or_else(|| " No versions found! ".to_owned()) + .map_or_else( + || " No versions found! ".to_owned(), + |n| n.loader.version.clone(), + ) } ); let fabric_json_text = file_utils::download_file_to_string(&url, false).await?; @@ -468,13 +470,13 @@ async fn create_minecraft_instance( } async fn mmc_forge( - sender: Option>>, + sender: Option<&Sender>, instance_selection: &InstanceSelection, version: Option, is_neoforge: bool, ) -> Result<(), InstancePackageError> { let (f_send, f_recv) = std::sync::mpsc::channel(); - if let Some(sender) = sender.clone() { + if let Some(sender) = sender.cloned() { std::thread::spawn(move || { pipe_progress(f_recv, &sender); }); diff --git a/quantum_launcher/src/cli/command.rs b/quantum_launcher/src/cli/command.rs index cb54381fc..7270e42d7 100644 --- a/quantum_launcher/src/cli/command.rs +++ b/quantum_launcher/src/cli/command.rs @@ -166,10 +166,10 @@ pub fn delete_instance( } } - let selected_instance = InstanceSelection::Instance(instance_name.clone()); - let deleted_instance_dir = selected_instance.get_instance_path(); + let instance = InstanceSelection::Instance(instance_name); + let deleted_instance_dir = instance.get_instance_path(); std::fs::remove_dir_all(&deleted_instance_dir)?; - info!("Deleted instance {instance_name}"); + info!("Deleted instance {}", instance.get_name()); Ok(()) } @@ -262,7 +262,7 @@ async fn refresh_account( tokio::task::spawn_blocking(|| { show_notification("Launching game", "Refreshing account..."); }); - }; + } match account.account_type.as_deref() { // Hook: Account types @@ -292,7 +292,7 @@ async fn refresh_account( tokio::task::spawn_blocking(|| { show_notification("Launching game", "Enjoy!"); }); - }; + } None }) } diff --git a/quantum_launcher/src/cli/mod.rs b/quantum_launcher/src/cli/mod.rs index b06408154..79d46fe03 100644 --- a/quantum_launcher/src/cli/mod.rs +++ b/quantum_launcher/src/cli/mod.rs @@ -259,7 +259,7 @@ pub fn start_cli(is_dir_err: bool, launcher_dir: &mut Option) { force, } => quit(command::delete_instance(instance_name, force)), QSubCommand::ListInstalled { properties } => { - quit(command::list_instances(properties.as_deref(), cli.server)) + quit(command::list_instances(properties.as_deref(), cli.server)); } QSubCommand::Loader(cmd) => { quit(runtime.block_on(command::loader(cmd, cli.server))); diff --git a/quantum_launcher/src/config.rs b/quantum_launcher/src/config.rs index e5e0e9727..c204a1c8b 100644 --- a/quantum_launcher/src/config.rs +++ b/quantum_launcher/src/config.rs @@ -215,10 +215,10 @@ impl LauncherConfig { } pub fn uses_system_decorations(&self) -> bool { + // change this to `is_some_and` when enabling the experimental decorations self.ui .as_ref() - .map(|n| matches!(n.window_decorations, UiWindowDecorations::System)) - .unwrap_or(true) // change this to false when enabling the experimental decorations + .is_none_or(|n| matches!(n.window_decorations, UiWindowDecorations::System)) } pub fn c_theme(&self) -> LauncherTheme { @@ -226,9 +226,7 @@ impl LauncherConfig { lightness: self.ui_mode.unwrap_or_default(), color: self.ui_theme.unwrap_or_default(), alpha: self.c_ui_opacity(), - system_dark_mode: dark_light::detect() - .map(|n| n == dark_light::Mode::Dark) - .unwrap_or_default(), + system_dark_mode: dark_light::detect().is_ok_and(|n| n == dark_light::Mode::Dark), } } diff --git a/quantum_launcher/src/menu_renderer/create.rs b/quantum_launcher/src/menu_renderer/create.rs index ddfeb4952..7ed4d669f 100644 --- a/quantum_launcher/src/menu_renderer/create.rs +++ b/quantum_launcher/src/menu_renderer/create.rs @@ -66,10 +66,7 @@ impl MenuCreateInstanceChoosing { .into() } - fn get_sidebar_contents<'a>( - &'a self, - timer: usize, - ) -> widget::Container<'a, Message, LauncherTheme> { + fn get_sidebar_contents(&self, timer: usize) -> widget::Container<'_, Message, LauncherTheme> { let header = self.get_sidebar_header(); let Some(versions) = &self.list else { diff --git a/quantum_launcher/src/menu_renderer/edit_instance.rs b/quantum_launcher/src/menu_renderer/edit_instance.rs index ef14b6f13..e73563023 100644 --- a/quantum_launcher/src/menu_renderer/edit_instance.rs +++ b/quantum_launcher/src/menu_renderer/edit_instance.rs @@ -353,8 +353,7 @@ Heavy modpacks / High settings: 4-8 GB+" self.config .custom_jar .as_ref() - .map(|n| n.name.clone()) - .unwrap_or(NONE_JAR_NAME.to_owned()), + .map_or_else(|| NONE_JAR_NAME.to_owned(), |n| n.name.clone()), ), |t| Message::EditInstance(EditInstanceMessage::CustomJarPathChanged(t)), ) @@ -391,7 +390,7 @@ Heavy modpacks / High settings: 4-8 GB+" Some(self.main_class_mode), |t| Message::EditInstance(EditInstanceMessage::SetMainClass( t, - Some("".to_owned()) + Some(String::new()) )) ) .size(14) @@ -485,16 +484,16 @@ pub fn get_args_list( args: Option<&[String]>, msg: impl Fn(ListMessage) -> Message + Clone + 'static, ) -> Element<'_> { - const ITEM_SIZE: u16 = 10; - - let args = args.unwrap_or_default(); - fn opt(icon: widget::Text<'_, LauncherTheme>) -> widget::Button<'_, Message, LauncherTheme> { widget::button(icon) .padding([6, 8]) .style(move |t: &LauncherTheme, s| t.style_button(s, StyleButton::FlatDark)) } + const ITEM_SIZE: u16 = 10; + + let args = args.unwrap_or_default(); + widget::Column::new() .push_maybe( (!args.is_empty()).then_some(widget::column(args.iter().enumerate().map( diff --git a/quantum_launcher/src/menu_renderer/mod.rs b/quantum_launcher/src/menu_renderer/mod.rs index 821a946b4..28ff1698e 100644 --- a/quantum_launcher/src/menu_renderer/mod.rs +++ b/quantum_launcher/src/menu_renderer/mod.rs @@ -174,7 +174,7 @@ pub fn button_with_icon<'a>( row![icon.into()] .push_maybe((!text.is_empty()).then_some(widget::text(text).size(size))) .align_y(Alignment::Center) - .spacing(size as f32 / 1.6), + .spacing(f32::from(size) / 1.6), ) .padding([7, 13]) } diff --git a/quantum_launcher/src/message_handler/iced_event.rs b/quantum_launcher/src/message_handler/iced_event.rs index 86568b4f9..7a3999660 100644 --- a/quantum_launcher/src/message_handler/iced_event.rs +++ b/quantum_launcher/src/message_handler/iced_event.rs @@ -147,10 +147,12 @@ impl Launcher { ]), // Search Action (general) - ("f", true, _, _, State::Create(MenuCreateInstance::Choosing { .. })) - | ("/", _, _, true, State::Create(MenuCreateInstance::Choosing { .. })) - | ("f", true, _, _, State::ModsDownload(_)) - | ("/", _, _, true, State::ModsDownload(_)) => Message::CoreFocusNext, + #[rustfmt::skip] + ("f", true, _, _, + State::Create(MenuCreateInstance::Choosing { .. }) | State::ModsDownload(_)) + | ("/", _, _, true, + State::Create(MenuCreateInstance::Choosing { .. }) | State::ModsDownload(_)) + => Message::CoreFocusNext, // Misc ("a", true, _, true, State::EditJarMods(_)) => { @@ -338,8 +340,10 @@ impl Launcher { } } State::InstallOptifine(MenuInstallOptifine::Choosing { .. }) - | State::InstallFabric(MenuInstallFabric::Loading { .. }) - | State::InstallFabric(MenuInstallFabric::Loaded { progress: None, .. }) + | State::InstallFabric( + MenuInstallFabric::Loading { .. } + | MenuInstallFabric::Loaded { progress: None, .. }, + ) | State::EditJarMods(_) | State::ExportMods(_) | State::ManagePresets(MenuEditPresets { diff --git a/quantum_launcher/src/message_handler/mod.rs b/quantum_launcher/src/message_handler/mod.rs index cf00953ce..fcd79a06f 100644 --- a/quantum_launcher/src/message_handler/mod.rs +++ b/quantum_launcher/src/message_handler/mod.rs @@ -450,7 +450,7 @@ impl Launcher { pub fn server_selected(&self) -> bool { self.selected_instance .as_ref() - .is_some_and(|n| n.is_server()) + .is_some_and(InstanceSelection::is_server) || if let State::Launch(menu) = &self.state { menu.is_viewing_server } else if let State::Create(MenuCreateInstance::Choosing( @@ -598,7 +598,7 @@ impl Launcher { } else { let future = stdin.write_all("stop\n".as_bytes()); _ = block_on(future); - }; + } } } } diff --git a/quantum_launcher/src/message_update/accounts.rs b/quantum_launcher/src/message_update/accounts.rs index ef7773f56..175648dba 100644 --- a/quantum_launcher/src/message_update/accounts.rs +++ b/quantum_launcher/src/message_update/accounts.rs @@ -316,7 +316,7 @@ impl Launcher { }, ); - self.account_selected = username.clone(); + self.account_selected.clone_from(&username); self.accounts.insert(username.clone(), data); self.go_to_launch_screen::(None) diff --git a/quantum_launcher/src/message_update/create_instance.rs b/quantum_launcher/src/message_update/create_instance.rs index 2b37ac2b1..e47416adc 100644 --- a/quantum_launcher/src/message_update/create_instance.rs +++ b/quantum_launcher/src/message_update/create_instance.rs @@ -61,7 +61,7 @@ impl Launcher { .or(iter().next())) { *selected_version = sel.clone(); } - }) + }); } CreateInstanceMessage::SidebarResize(ratio) => { let window_width = self.window_state.size.0; @@ -158,13 +158,12 @@ then go to "Mods->Add File""#, .enumerate() .filter(|n| n.1.kind != ListEntryKind::Snapshot) .find(|n| n.1.name == latest) - .map(|n| { + .map_or_else(|| ListEntry::new(latest), |n| { offset = n.0 as f32 / len as f32; n.1.clone() - }) - .unwrap_or_else(|| ListEntry::new(latest)); + }); *list = Some(versions); - }) + }); } pub fn go_to_create_screen(&mut self, is_server: bool) -> Task { diff --git a/quantum_launcher/src/message_update/edit_instance.rs b/quantum_launcher/src/message_update/edit_instance.rs index 86f28f8c5..79afb6e88 100644 --- a/quantum_launcher/src/message_update/edit_instance.rs +++ b/quantum_launcher/src/message_update/edit_instance.rs @@ -83,7 +83,7 @@ impl Launcher { EditInstanceMessage::JavaOverrideVersion(n) => { iflet_config!(&mut self.state, config <- { config.java_override_version = Some(n); - }) + }); } EditInstanceMessage::BrowseJavaOverride => { if let Some(file) = rfd::FileDialog::new() @@ -113,7 +113,6 @@ impl Launcher { .. }) = &mut self.state { - menu.memory_input = input.clone(); if let Ok(mb) = input.parse::() { if mb > 0 { menu.config.ram_in_mb = mb; @@ -121,6 +120,7 @@ impl Launcher { menu.slider_text = format_memory(mb); } } + menu.memory_input = input; } } EditInstanceMessage::LoggingToggle(t) => iflet_config!(&mut self.state, config <- { @@ -169,12 +169,11 @@ impl Launcher { .. }) = &mut self.state { - menu.instance_name = self - .selected_instance + self.selected_instance .as_ref() .unwrap() .get_name() - .to_owned(); + .clone_into(&mut menu.instance_name); menu.is_editing_name = !menu.is_editing_name; } } @@ -229,7 +228,7 @@ impl Launcher { menu.config .custom_jar .get_or_insert_with(CustomJarConfig::default) - .name = path + .name = path; } } } @@ -260,7 +259,7 @@ impl Launcher { if let Some(c) = &mut config.custom_jar { c.autoset_main_class = autos; } - }; + } } EditInstanceMessage::ReinstallLibraries => { return Ok(self.instance_redownload_stage( @@ -299,26 +298,7 @@ impl Launcher { ) } - fn loaded_custom_jar(&mut self, items: Vec) -> Task { - match &mut self.custom_jar { - Some(cx) => { - cx.choices = items.clone(); - } - None => { - let (recv, watcher) = match dir_watch(LAUNCHER_DIR.join("custom_jars")) { - Ok(n) => n, - Err(err) => { - err!("Couldn't load list of custom jars (2)! {err}"); - return Task::none(); - } - }; - self.custom_jar = Some(CustomJarState { - choices: items.clone(), - recv, - _watcher: watcher, - }); - } - } + fn loaded_custom_jar(&mut self, choices: Vec) -> Task { // If the currently selected jar got deleted/renamed // then unselect it if let State::Launch(MenuLaunch { @@ -327,11 +307,29 @@ impl Launcher { }) = &mut self.state { if let Some(jar) = &menu.config.custom_jar { - if !items.contains(&jar.name) { + if !choices.contains(&jar.name) { menu.config.custom_jar = None; } } } + + if let Some(cx) = &mut self.custom_jar { + cx.choices = choices; + } else { + let (recv, watcher) = match dir_watch(LAUNCHER_DIR.join("custom_jars")) { + Ok(n) => n, + Err(err) => { + err!("Couldn't load list of custom jars (2)! {err}"); + return Task::none(); + } + }; + self.custom_jar = Some(CustomJarState { + choices, + recv, + _watcher: watcher, + }); + } + Task::none() } diff --git a/quantum_launcher/src/message_update/manage_mods.rs b/quantum_launcher/src/message_update/manage_mods.rs index d9795e25f..4b17f4ab0 100644 --- a/quantum_launcher/src/message_update/manage_mods.rs +++ b/quantum_launcher/src/message_update/manage_mods.rs @@ -505,7 +505,7 @@ impl Launcher { fn manage_jarmods_toggle_selected(&mut self) { if let State::EditJarMods(menu) = &mut self.state { - for selected in menu.selected_mods.iter() { + for selected in &menu.selected_mods { if let Some(jarmod) = menu .jarmods .mods @@ -527,7 +527,7 @@ impl Launcher { .get_instance_path() .join("jarmods"); - for selected in menu.selected_mods.iter() { + for selected in &menu.selected_mods { if let Some(n) = menu .jarmods .mods diff --git a/quantum_launcher/src/message_update/shortcuts.rs b/quantum_launcher/src/message_update/shortcuts.rs index f86b89a71..e9d5bbeb7 100644 --- a/quantum_launcher/src/message_update/shortcuts.rs +++ b/quantum_launcher/src/message_update/shortcuts.rs @@ -107,7 +107,7 @@ impl Launcher { self.state = State::CreateShortcut(MenuShortcut { shortcut: Shortcut { name: self.instance().get_name().to_owned(), - description: "".to_owned(), + description: String::new(), exec: String::new(), exec_args: vec![], icon: None, @@ -116,7 +116,7 @@ impl Launcher { add_to_desktop: false, account: self.account_selected.clone(), account_offline: self.config.username.clone(), - }) + }); } pub fn shortcut_prepare(&mut self) -> Result { diff --git a/quantum_launcher/src/state/mod.rs b/quantum_launcher/src/state/mod.rs index dbfbc69df..a4e578dd3 100644 --- a/quantum_launcher/src/state/mod.rs +++ b/quantum_launcher/src/state/mod.rs @@ -153,7 +153,7 @@ impl Launcher { launch } else { if let Err(err) = migration(version) { - err!(no_log, "{err}") + err!(no_log, "{err}"); } config.version = Some(LAUNCHER_VERSION_NAME.to_owned()); State::ChangeLog diff --git a/quantum_launcher/src/stylesheet/widgets.rs b/quantum_launcher/src/stylesheet/widgets.rs index 91fbfb189..ec1b5b906 100644 --- a/quantum_launcher/src/stylesheet/widgets.rs +++ b/quantum_launcher/src/stylesheet/widgets.rs @@ -216,13 +216,15 @@ impl widget::pick_list::Catalog for LauncherTheme { fn style(&self, (): &(), status: widget::pick_list::Status) -> widget::pick_list::Style { match status { - widget::pick_list::Status::Active => widget::pick_list::Style { - text_color: self.get(Color::Light), - placeholder_color: self.get(Color::SecondLight), - handle_color: self.get(Color::Light), - background: self.get_bg(Color::Dark), - border: self.get_border(Color::SecondDark), - }, + widget::pick_list::Status::Active | widget::pick_list::Status::Opened => { + widget::pick_list::Style { + text_color: self.get(Color::Light), + placeholder_color: self.get(Color::SecondLight), + handle_color: self.get(Color::Light), + background: self.get_bg(Color::Dark), + border: self.get_border(Color::SecondDark), + } + } widget::pick_list::Status::Hovered => widget::pick_list::Style { text_color: self.get(Color::Light), placeholder_color: self.get(Color::SecondLight), @@ -230,13 +232,6 @@ impl widget::pick_list::Catalog for LauncherTheme { background: self.get_bg(Color::SecondDark), border: self.get_border(Color::SecondDark), }, - widget::pick_list::Status::Opened => widget::pick_list::Style { - text_color: self.get(Color::Light), - placeholder_color: self.get(Color::SecondLight), - handle_color: self.get(Color::Light), - background: self.get_bg(Color::Dark), - border: self.get_border(Color::SecondDark), - }, } } } diff --git a/quantum_launcher/src/update.rs b/quantum_launcher/src/update.rs index 2e2ab0b09..b591c515c 100644 --- a/quantum_launcher/src/update.rs +++ b/quantum_launcher/src/update.rs @@ -237,7 +237,7 @@ impl Launcher { } } Message::CoreListLoaded(Ok((list, is_server))) => { - self.core_list_loaded(list, is_server) + self.core_list_loaded(list, is_server); } Message::CoreCopyText(txt) => { return iced::clipboard::write(txt); From fd497052e20f778b41a4e49a2d971ea0c84d7482 Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Sun, 22 Feb 2026 21:14:54 +0530 Subject: [PATCH 27/30] Dev: Use RAII for Windows COM APIs (shortcut creation), disable icon field (unused) --- Cargo.toml | 2 +- crates/ezshortcut/src/lib.rs | 2 +- crates/ezshortcut/src/os/windows/comguard.rs | 34 +++++++++++++++++++ .../src/os/{windows.rs => windows/mod.rs} | 25 ++++++-------- .../src/message_update/shortcuts.rs | 2 +- 5 files changed, 48 insertions(+), 17 deletions(-) create mode 100644 crates/ezshortcut/src/os/windows/comguard.rs rename crates/ezshortcut/src/os/{windows.rs => windows/mod.rs} (87%) diff --git a/Cargo.toml b/Cargo.toml index 3ab164e99..7fd7263f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ reqwest = { version = "0.12", default-features = false, features = [ "http2", ] } -tokio = { version = "1", features = ["fs", "macros", "process"] } +tokio = { version = "1", features = ["fs", "macros", "process", "rt", "rt-multi-thread"] } futures = { version = "0.3", default-features = false, features = ["executor"] } cfg-if = "1" diff --git a/crates/ezshortcut/src/lib.rs b/crates/ezshortcut/src/lib.rs index 5da84c8a1..7b070d071 100644 --- a/crates/ezshortcut/src/lib.rs +++ b/crates/ezshortcut/src/lib.rs @@ -15,7 +15,7 @@ pub struct Shortcut { pub description: String, pub exec: String, pub exec_args: Vec, - pub icon: Option, + // pub icon: Option, } impl Shortcut { diff --git a/crates/ezshortcut/src/os/windows/comguard.rs b/crates/ezshortcut/src/os/windows/comguard.rs new file mode 100644 index 000000000..c60d88d15 --- /dev/null +++ b/crates/ezshortcut/src/os/windows/comguard.rs @@ -0,0 +1,34 @@ +use windows::Win32::System::Com::{CoInitializeEx, CoUninitialize, COINIT_APARTMENTTHREADED}; + +pub struct ComGuard { + should_uninit: bool, +} + +impl ComGuard { + pub fn new() -> windows::core::Result { + use windows::Win32::Foundation::RPC_E_CHANGED_MODE; + + unsafe { + match CoInitializeEx(None, COINIT_APARTMENTTHREADED).ok() { + Ok(()) => Ok(Self { + should_uninit: true, + }), + Err(e) if e.code() == RPC_E_CHANGED_MODE => { + // Already initialized with different model — OK + Ok(Self { + should_uninit: false, + }) + } + Err(e) => Err(e), + } + } + } +} + +impl Drop for ComGuard { + fn drop(&mut self) { + if self.should_uninit { + unsafe { CoUninitialize() }; + } + } +} diff --git a/crates/ezshortcut/src/os/windows.rs b/crates/ezshortcut/src/os/windows/mod.rs similarity index 87% rename from crates/ezshortcut/src/os/windows.rs rename to crates/ezshortcut/src/os/windows/mod.rs index eaeb105e6..9828ba29f 100644 --- a/crates/ezshortcut/src/os/windows.rs +++ b/crates/ezshortcut/src/os/windows/mod.rs @@ -2,27 +2,28 @@ use tokio::{fs, task::spawn_blocking}; use windows::{ core::{Interface, HSTRING}, Win32::{ - System::Com::{ - CoCreateInstance, CoInitializeEx, CoTaskMemFree, CoUninitialize, IPersistFile, - CLSCTX_INPROC_SERVER, COINIT_APARTMENTTHREADED, - }, + System::Com::{CoCreateInstance, CoTaskMemFree, IPersistFile, CLSCTX_INPROC_SERVER}, UI::Shell::{ FOLDERID_Programs, IShellLinkW, SHGetKnownFolderPath, ShellLink, KNOWN_FOLDER_FLAG, }, }, }; -use crate::Shortcut; +use crate::{os::windows::comguard::ComGuard, Shortcut}; use std::path::{Path, PathBuf}; +mod comguard; + +/// Fetches path to the Start Menu entries folder #[must_use] pub fn get_menu_path() -> Option { unsafe { let path_ptr = SHGetKnownFolderPath(&FOLDERID_Programs, KNOWN_FOLDER_FLAG::default(), None).ok()?; - let path = path_ptr.to_string().ok()?; + let path = path_ptr.to_string().ok(); CoTaskMemFree(Some(path_ptr.0 as _)); + let path = path?; // Avoid memory leak if error occurs Some(std::path::PathBuf::from(path).join("QuantumLauncher")) } } @@ -47,14 +48,12 @@ fn create_inner(shortcut: &Shortcut, path: PathBuf) -> std::io::Result<()> { .collect::>() .join(" "); - unsafe { - CoInitializeEx(None, COINIT_APARTMENTTHREADED) - .ok() - .map_err(ioerr)?; + let _com = ComGuard::new().map_err(ioerr)?; + unsafe { let shell_link: IShellLinkW = CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER)?; shell_link.SetPath(&HSTRING::from(&shortcut.exec))?; - if !args.trim().is_empty() { + if !args.is_empty() { shell_link.SetArguments(&HSTRING::from(args))?; } if !shortcut.description.trim().is_empty() { @@ -62,9 +61,7 @@ fn create_inner(shortcut: &Shortcut, path: PathBuf) -> std::io::Result<()> { } let persist: IPersistFile = shell_link.cast()?; - persist.Save(&HSTRING::from(path.as_path()), true)?; - - CoUninitialize(); + persist.Save(&HSTRING::from(path.as_path()), true)?; // true means overwrite } Ok(()) diff --git a/quantum_launcher/src/message_update/shortcuts.rs b/quantum_launcher/src/message_update/shortcuts.rs index e9d5bbeb7..b28fe73dc 100644 --- a/quantum_launcher/src/message_update/shortcuts.rs +++ b/quantum_launcher/src/message_update/shortcuts.rs @@ -110,7 +110,7 @@ impl Launcher { description: String::new(), exec: String::new(), exec_args: vec![], - icon: None, + // icon: None, }, add_to_menu: true, add_to_desktop: false, From 76c4a6a45d3777c3368bb392569d0c378cfb6a34 Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Sun, 22 Feb 2026 21:15:24 +0530 Subject: [PATCH 28/30] Docs: Document ezshortcut, other doc/comment tweaks --- crates/ezshortcut/README.md | 18 ++++++++++++++++++ crates/ezshortcut/src/lib.rs | 16 +++++++++++++++- crates/ezshortcut/src/os/macos.rs | 1 + crates/ezshortcut/src/os/mod.rs | 6 ++++++ crates/ezshortcut/src/os/unix.rs | 1 + crates/ql_core/src/file_utils.rs | 3 +++ quantum_launcher/src/main.rs | 3 +-- 7 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 crates/ezshortcut/README.md diff --git a/crates/ezshortcut/README.md b/crates/ezshortcut/README.md new file mode 100644 index 000000000..880a72300 --- /dev/null +++ b/crates/ezshortcut/README.md @@ -0,0 +1,18 @@ +# ezshortcut + +A cross-platform library for creating desktop/app shortcuts. +See crate documentation for more information. + +## Features + +- Create shortcuts for common platforms +- Add shortcut to Start Menu/Applications/etc, and attempt to refresh list +- Automatic escaping/sanitization of names/paths + +## Platforms + +- Windows (`Win32`) +- macOS (`.app`) +- Linux/BSD/Unix (`.desktop`) + +TODO: Implement icons diff --git a/crates/ezshortcut/src/lib.rs b/crates/ezshortcut/src/lib.rs index 7b070d071..d257e1fb3 100644 --- a/crates/ezshortcut/src/lib.rs +++ b/crates/ezshortcut/src/lib.rs @@ -1,25 +1,39 @@ +#![doc = include_str!("../README.md")] + use std::path::{Path, PathBuf}; mod os; pub use os::{get_menu_path, EXTENSION, EXTENSION_S}; +/// Fetches the path to user's `Desktop` folder #[must_use] pub fn get_desktop_dir() -> Option { dirs::desktop_dir().or_else(|| dirs::home_dir().map(|n| n.join("Desktop"))) } +/// Information about the desktop shortcut to create. **Use this to create shortcuts!** #[derive(Debug, Clone)] pub struct Shortcut { + /// Name of the shortcut. + /// + /// A sanitized version of this will be used as the filename, + /// and the regular version will be the display name if possible. pub name: String, /// Leave blank for none. Unsupported on macOS pub description: String, + /// The executable binary to use. + /// + /// *No need for quotes/escaping! We will handle that* pub exec: String, + /// The arguments to pass to the executable. + /// + /// *No need for quotes/escaping! We will handle that* pub exec_args: Vec, // pub icon: Option, } impl Shortcut { - /// Create the shortcut and save it to `path`. + /// Creates the shortcut and save it to `path`. /// /// Note: On Linux, if you just place the shortcut anywhere, /// it may not work. You may need to save it to system-wide locations diff --git a/crates/ezshortcut/src/os/macos.rs b/crates/ezshortcut/src/os/macos.rs index e9cc37964..8668bddc4 100644 --- a/crates/ezshortcut/src/os/macos.rs +++ b/crates/ezshortcut/src/os/macos.rs @@ -3,6 +3,7 @@ use tokio::{fs, process::Command}; use crate::{make_filename_safe, Shortcut}; use std::path::{Path, PathBuf}; +/// Fetches path to the Applications folder #[must_use] pub fn get_menu_path() -> Option { dirs::home_dir().map(|h| h.join("Applications")) diff --git a/crates/ezshortcut/src/os/mod.rs b/crates/ezshortcut/src/os/mod.rs index 88c036f27..95b50b9bf 100644 --- a/crates/ezshortcut/src/os/mod.rs +++ b/crates/ezshortcut/src/os/mod.rs @@ -4,17 +4,23 @@ cfg_if!( if #[cfg(target_os = "windows")] { mod windows; pub use windows::*; + /// `.lnk`/`.app`/`.desktop` extension pub const EXTENSION: &str = ".lnk"; + /// `lnk`/`app`/`desktop` extension (no period) pub const EXTENSION_S: &str = "lnk"; } else if #[cfg(target_os = "macos")] { mod macos; pub use macos::*; + /// `.lnk`/`.app`/`.desktop` extension pub const EXTENSION: &str = ".app"; + /// `lnk`/`app`/`desktop` extension (no period) pub const EXTENSION_S: &str = "app"; } else if #[cfg(target_family = "unix")] { mod unix; pub use unix::*; + /// `.lnk`/`.app`/`.desktop` extension pub const EXTENSION: &str = ".desktop"; + /// `lnk`/`app`/`desktop` extension (no period) pub const EXTENSION_S: &str = "desktop"; } else { mod stub; diff --git a/crates/ezshortcut/src/os/unix.rs b/crates/ezshortcut/src/os/unix.rs index e465945a3..12a9d564e 100644 --- a/crates/ezshortcut/src/os/unix.rs +++ b/crates/ezshortcut/src/os/unix.rs @@ -7,6 +7,7 @@ use tokio::fs; use crate::Shortcut; +/// Fetches path to the `.desktop` files' folder (Applications Menu) #[must_use] pub fn get_menu_path() -> Option { if let Ok(dir) = std::env::var("XDG_DATA_HOME") { diff --git a/crates/ql_core/src/file_utils.rs b/crates/ql_core/src/file_utils.rs index 97a5c0c25..37a88efea 100644 --- a/crates/ql_core/src/file_utils.rs +++ b/crates/ql_core/src/file_utils.rs @@ -108,6 +108,9 @@ fn check_qlportable_file() -> Option { .map(|s| s.trim().to_lowercase()) .collect(); + // Safety: At this specific moment, nothing else + // would read/write these env vars. This function + // is called at launcher startup on the main thread. unsafe { if flags.contains("i_vulkan") { std::env::set_var("WGPU_BACKEND", "vulkan"); diff --git a/quantum_launcher/src/main.rs b/quantum_launcher/src/main.rs index 4b3d9ad50..7036540e8 100644 --- a/quantum_launcher/src/main.rs +++ b/quantum_launcher/src/main.rs @@ -264,8 +264,7 @@ fn load_fonts() -> Vec> { ] } -/// This is the only `unsafe` Rust code in the entire launcher. -/// It tweaks Windows terminal behaviour so that: +/// Tweaks Windows terminal behaviour so that: /// /// - If launcher is opened from terminal, /// it shows output in terminal From 9c332047974daf3c8f38c2ebaf4edf9883b3640e Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Sun, 22 Feb 2026 21:22:54 +0530 Subject: [PATCH 29/30] Fix: bring back icon field and implement for windows --- crates/ezshortcut/src/lib.rs | 5 ++++- crates/ezshortcut/src/os/unix.rs | 6 ++---- crates/ezshortcut/src/os/windows/mod.rs | 3 +++ quantum_launcher/src/message_update/shortcuts.rs | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/ezshortcut/src/lib.rs b/crates/ezshortcut/src/lib.rs index d257e1fb3..a50c5e102 100644 --- a/crates/ezshortcut/src/lib.rs +++ b/crates/ezshortcut/src/lib.rs @@ -29,7 +29,10 @@ pub struct Shortcut { /// /// *No need for quotes/escaping! We will handle that* pub exec_args: Vec, - // pub icon: Option, + /// Path to icon, leave empty for none. + /// + /// Unimplemented on macOS currently (TODO) + pub icon: String, } impl Shortcut { diff --git a/crates/ezshortcut/src/os/unix.rs b/crates/ezshortcut/src/os/unix.rs index 12a9d564e..70e967f75 100644 --- a/crates/ezshortcut/src/os/unix.rs +++ b/crates/ezshortcut/src/os/unix.rs @@ -64,10 +64,8 @@ Categories=Game;", format!("Comment={desc}\n") }, exec = shortcut.exec, - icon = shortcut - .icon - .as_deref() - .map(|n| format!("Icon={n}\n")) + icon = (!shortcut.icon.is_empty()) + .then(|| format!("Icon={}\n", shortcut.icon)) .unwrap_or_default() ); diff --git a/crates/ezshortcut/src/os/windows/mod.rs b/crates/ezshortcut/src/os/windows/mod.rs index 9828ba29f..24a3ad37a 100644 --- a/crates/ezshortcut/src/os/windows/mod.rs +++ b/crates/ezshortcut/src/os/windows/mod.rs @@ -59,6 +59,9 @@ fn create_inner(shortcut: &Shortcut, path: PathBuf) -> std::io::Result<()> { if !shortcut.description.trim().is_empty() { shell_link.SetDescription(&HSTRING::from(&shortcut.description))?; } + if !shortcut.icon.trim().is_empty() { + shell_link.SetIconLocation(&HSTRING::from(&shortcut.icon), 0)?; + } let persist: IPersistFile = shell_link.cast()?; persist.Save(&HSTRING::from(path.as_path()), true)?; // true means overwrite diff --git a/quantum_launcher/src/message_update/shortcuts.rs b/quantum_launcher/src/message_update/shortcuts.rs index b28fe73dc..3fd405a54 100644 --- a/quantum_launcher/src/message_update/shortcuts.rs +++ b/quantum_launcher/src/message_update/shortcuts.rs @@ -110,7 +110,7 @@ impl Launcher { description: String::new(), exec: String::new(), exec_args: vec![], - // icon: None, + icon: String::new(), }, add_to_menu: true, add_to_desktop: false, From c84b3ea2be6f72e8fe56474a7b8884d04a28519a Mon Sep 17 00:00:00 2001 From: Mrmayman Date: Sun, 22 Feb 2026 22:47:20 +0530 Subject: [PATCH 30/30] Fix clippy lints --- crates/ezshortcut/src/os/unix.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/ezshortcut/src/os/unix.rs b/crates/ezshortcut/src/os/unix.rs index 70e967f75..ee57048da 100644 --- a/crates/ezshortcut/src/os/unix.rs +++ b/crates/ezshortcut/src/os/unix.rs @@ -64,9 +64,11 @@ Categories=Game;", format!("Comment={desc}\n") }, exec = shortcut.exec, - icon = (!shortcut.icon.is_empty()) - .then(|| format!("Icon={}\n", shortcut.icon)) - .unwrap_or_default() + icon = if shortcut.icon.is_empty() { + String::new() + } else { + format!("Icon={}\n", shortcut.icon) + } ); match fs::metadata(path).await {