refactor: idiomatic Rust cleanup and quality improvements

- Replace .map().unwrap_or(false) with .is_some_and()/.is_ok_and()
- Use path.display() instead of {:?} for user-facing messages
- Replace Option<Option<Vec<String>>> with GamescopeUpdate enum
- Replace manual parent-walking loops with .ancestors() iterators
- Simplify kill()/kill_all() signatures to return () instead of Result
- Use tokio::task::spawn_blocking instead of hand-rolled thread+oneshot
- Read /proc/self/status for UID instead of spawning id subprocess
- Build Exec= line directly in render_desktop instead of string-replace
- Bump PKGBUILD pkgrel to 6
This commit is contained in:
funman300
2026-04-19 11:29:42 -07:00
parent 8447581fe6
commit 2f4f1c64d2
13 changed files with 86 additions and 98 deletions
Generated
+1
View File
@@ -4439,6 +4439,7 @@ dependencies = [
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"tokio",
"toml", "toml",
] ]
+1
View File
@@ -45,3 +45,4 @@ reqwest = { version = "0.12", features = ["blocking", "json"] }
# GUI for the setup wizard # GUI for the setup wizard
iced = { version = "0.13", features = ["tokio"] } iced = { version = "0.13", features = ["tokio"] }
iced_fonts = { version = "0.1", features = ["bootstrap"] } iced_fonts = { version = "0.1", features = ["bootstrap"] }
tokio = { version = "1.52.1", features = ["rt"] }
+1 -1
View File
@@ -12,7 +12,7 @@
pkgname=umutray pkgname=umutray
pkgver=0.1.0 pkgver=0.1.0
pkgrel=4 pkgrel=6
pkgdesc='Tray-based Wine launcher manager for Linux via umu/Proton-GE' pkgdesc='Tray-based Wine launcher manager for Linux via umu/Proton-GE'
arch=('x86_64') arch=('x86_64')
url='https://git.aleshym.co/funman300/umutray' url='https://git.aleshym.co/funman300/umutray'
+20 -8
View File
@@ -4,6 +4,17 @@ use owo_colors::OwoColorize;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::PathBuf; use std::path::PathBuf;
/// Expresses the desired change to a game's gamescope setting.
#[derive(Debug, Clone)]
pub enum GamescopeUpdate {
/// Leave the current value unchanged.
Unchanged,
/// Disable gamescope.
Disable,
/// Enable gamescope with the given CLI arguments.
Enable(Vec<String>),
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Launcher { pub struct Launcher {
/// Short CLI name (e.g. "battlenet"). /// Short CLI name (e.g. "battlenet").
@@ -236,7 +247,7 @@ impl Config {
return Ok(c); return Ok(c);
} }
let content = std::fs::read_to_string(&path) let content = std::fs::read_to_string(&path)
.with_context(|| format!("Failed to read config from {path:?}"))?; .with_context(|| format!("Failed to read config from {}", path.display()))?;
match toml::from_str::<Self>(&content) { match toml::from_str::<Self>(&content) {
Ok(c) => { Ok(c) => {
Ok(c) Ok(c)
@@ -244,7 +255,7 @@ impl Config {
Err(e) => { Err(e) => {
let bak = path.with_extension("toml.bak"); let bak = path.with_extension("toml.bak");
std::fs::rename(&path, &bak) std::fs::rename(&path, &bak)
.with_context(|| format!("Failed to back up stale config to {bak:?}"))?; .with_context(|| format!("Failed to back up stale config to {}", bak.display()))?;
eprintln!("warning: couldn't parse {}: {e}", path.display()); eprintln!("warning: couldn't parse {}: {e}", path.display());
eprintln!( eprintln!(
" backed up to {} — writing fresh config with presets", " backed up to {} — writing fresh config with presets",
@@ -264,7 +275,7 @@ impl Config {
} }
let content = toml::to_string_pretty(self)?; let content = toml::to_string_pretty(self)?;
std::fs::write(&path, content) std::fs::write(&path, content)
.with_context(|| format!("Failed to write config to {path:?}")) .with_context(|| format!("Failed to write config to {}", path.display()))
} }
pub fn find(&self, name: &str) -> Option<&Launcher> { pub fn find(&self, name: &str) -> Option<&Launcher> {
@@ -386,16 +397,15 @@ impl Config {
} }
/// Update per-game overlay flags. Each arg is `None` = leave as-is. /// Update per-game overlay flags. Each arg is `None` = leave as-is.
/// `gamescope = Some(None)` disables it; `Some(Some(vec))` enables with args.
pub fn set_game_flags( pub fn set_game_flags(
&mut self, &mut self,
launcher: &str, launcher: &str,
name: &str, name: &str,
gamemode: Option<bool>, gamemode: Option<bool>,
mangohud: Option<bool>, mangohud: Option<bool>,
gamescope: Option<Option<Vec<String>>>, gamescope: GamescopeUpdate,
) -> Result<()> { ) -> Result<()> {
if gamemode.is_none() && mangohud.is_none() && gamescope.is_none() { if gamemode.is_none() && mangohud.is_none() && matches!(gamescope, GamescopeUpdate::Unchanged) {
anyhow::bail!( anyhow::bail!(
"nothing to set — pass --gamemode, --mangohud, --gamescope, or --no-gamescope" "nothing to set — pass --gamemode, --mangohud, --gamescope, or --no-gamescope"
); );
@@ -415,8 +425,10 @@ impl Config {
if let Some(v) = mangohud { if let Some(v) = mangohud {
g.mangohud = v; g.mangohud = v;
} }
if let Some(v) = gamescope { match gamescope {
g.gamescope = v; GamescopeUpdate::Unchanged => {}
GamescopeUpdate::Disable => g.gamescope = None,
GamescopeUpdate::Enable(args) => g.gamescope = Some(args),
} }
self.save()?; self.save()?;
println!("{} Updated flags for '{launcher}/{name}'.", "".green().bold()); println!("{} Updated flags for '{launcher}/{name}'.", "".green().bold());
+6 -21
View File
@@ -23,8 +23,7 @@ pub fn scan_for_gui(config: &Config) -> Vec<DetectHit> {
if prefix.join("drive_c").join(&preset.exe_path).exists() { if prefix.join("drive_c").join(&preset.exe_path).exists() {
let configured = config let configured = config
.find(&preset.name) .find(&preset.name)
.map(|l| l.prefix_dir == *prefix) .is_some_and(|l| l.prefix_dir == *prefix);
.unwrap_or(false);
hits.push(DetectHit { hits.push(DetectHit {
display: preset.display.clone(), display: preset.display.clone(),
prefix: prefix.clone(), prefix: prefix.clone(),
@@ -182,14 +181,7 @@ fn parse_heroic_gog_installed(text: &str, map: &mut HashMap<PathBuf, String>) {
/// Walk up from `exe_path` checking each ancestor against `STORE_TITLES`. /// Walk up from `exe_path` checking each ancestor against `STORE_TITLES`.
fn store_title(exe_path: &Path) -> Option<String> { fn store_title(exe_path: &Path) -> Option<String> {
let mut dir = exe_path.parent(); exe_path.ancestors().skip(1).find_map(|d| STORE_TITLES.get(d).cloned())
while let Some(d) = dir {
if let Some(title) = STORE_TITLES.get(d) {
return Some(title.clone());
}
dir = d.parent();
}
None
} }
/// Scan a launcher's Wine prefix for installed game executables. /// Scan a launcher's Wine prefix for installed game executables.
@@ -251,8 +243,7 @@ fn scan_exe_dir(
} else if path } else if path
.extension() .extension()
.and_then(|e| e.to_str()) .and_then(|e| e.to_str())
.map(|e| e.eq_ignore_ascii_case("exe")) .is_some_and(|e| e.eq_ignore_ascii_case("exe"))
.unwrap_or(false)
{ {
let Ok(rel) = path.strip_prefix(drive_c) else { continue }; let Ok(rel) = path.strip_prefix(drive_c) else { continue };
let rel_str = rel.to_string_lossy().to_string(); let rel_str = rel.to_string_lossy().to_string();
@@ -344,20 +335,16 @@ fn resolve_uncached(exe_path: &Path) -> String {
/// - GOG: `goggame-<id>.info` → `{ "gameName": "..." }` /// - GOG: `goggame-<id>.info` → `{ "gameName": "..." }`
/// - Epic: `.egstore/<id>.item` → `{ "DisplayName": "..." }` /// - Epic: `.egstore/<id>.item` → `{ "DisplayName": "..." }`
fn read_manifest_name(exe_path: &Path) -> Option<String> { fn read_manifest_name(exe_path: &Path) -> Option<String> {
let mut dir = exe_path.parent(); for d in exe_path.ancestors().skip(1) {
while let Some(d) = dir {
let dirname = d.file_name().and_then(|n| n.to_str()).unwrap_or("").to_lowercase(); let dirname = d.file_name().and_then(|n| n.to_str()).unwrap_or("").to_lowercase();
// Stop once we reach drive_c root or the Program Files tier — manifests // Stop once we reach drive_c root or the Program Files tier — manifests
// are never above the game's installation folder. // are never above the game's installation folder.
if dirname == "drive_c" || dirname.starts_with("program files") { if dirname == "drive_c" || dirname.starts_with("program files") {
break; break;
} }
if let Some(name) = read_gog_manifest(d).or_else(|| read_epic_manifest(d)) { if let Some(name) = read_gog_manifest(d).or_else(|| read_epic_manifest(d)) {
return Some(name); return Some(name);
} }
dir = d.parent();
} }
None None
} }
@@ -438,8 +425,7 @@ fn nearest_dir_name(path: &Path) -> String {
"launcher", "engine", "client", "launcher", "engine", "client",
]; ];
let mut dir = path.parent(); for d in path.ancestors().skip(1) {
while let Some(d) = dir {
let name = d.file_name().and_then(|n| n.to_str()).unwrap_or(""); let name = d.file_name().and_then(|n| n.to_str()).unwrap_or("");
let lower = name.to_lowercase(); let lower = name.to_lowercase();
if !name.is_empty() if !name.is_empty()
@@ -448,7 +434,6 @@ fn nearest_dir_name(path: &Path) -> String {
{ {
return name.to_string(); return name.to_string();
} }
dir = d.parent();
} }
// Nothing useful in the path — return the exe stem as-is. // Nothing useful in the path — return the exe stem as-is.
@@ -529,7 +514,7 @@ fn collect_prefixes(dir: &Path, depth: u32, out: &mut Vec<PathBuf>) {
return; return;
}; };
for entry in entries.flatten() { for entry in entries.flatten() {
if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) { if entry.file_type().is_ok_and(|t| t.is_dir()) {
collect_prefixes(&entry.path(), depth + 1, out); collect_prefixes(&entry.path(), depth + 1, out);
} }
} }
+14 -14
View File
@@ -80,8 +80,7 @@ fn global_vulkan_check() -> CheckResult {
.stdout(Stdio::null()) .stdout(Stdio::null())
.stderr(Stdio::null()) .stderr(Stdio::null())
.status() .status()
.map(|s| s.success()) .is_ok_and(|s| s.success());
.unwrap_or(false);
if ok { if ok {
CheckResult::pass("vulkan", "vulkaninfo OK") CheckResult::pass("vulkan", "vulkaninfo OK")
} else { } else {
@@ -218,8 +217,7 @@ fn count_ge_proton(dir: &Path) -> usize {
.filter(|e| { .filter(|e| {
e.file_name() e.file_name()
.to_str() .to_str()
.map(|s| s.starts_with("GE-Proton")) .is_some_and(|s| s.starts_with("GE-Proton"))
.unwrap_or(false)
}) })
.count() .count()
}) })
@@ -236,19 +234,21 @@ fn which(cmd: &str) -> Option<String> {
.map(|s| s.trim().to_string()) .map(|s| s.trim().to_string())
} }
fn current_uid() -> Option<u32> {
std::fs::read_to_string("/proc/self/status")
.ok()
.and_then(|s| {
s.lines()
.find(|l| l.starts_with("Uid:"))
.and_then(|l| l.split_whitespace().nth(1))
.and_then(|s| s.parse().ok())
})
}
fn is_owned_by_current_user(path: &Path) -> bool { fn is_owned_by_current_user(path: &Path) -> bool {
let file_uid = match std::fs::metadata(path) { let file_uid = match std::fs::metadata(path) {
Ok(m) => m.uid(), Ok(m) => m.uid(),
Err(_) => return true, Err(_) => return true,
}; };
let current_uid: Option<u32> = Command::new("id") current_uid().map_or(true, |uid| uid == file_uid)
.arg("-u")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.and_then(|s| s.trim().parse().ok());
match current_uid {
Some(uid) => uid == file_uid,
None => true,
}
} }
+5 -7
View File
@@ -171,8 +171,7 @@ fn update(state: &mut Dashboard, msg: Message) -> Task<Message> {
.stdout(std::process::Stdio::null()) .stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null()) .stderr(std::process::Stdio::null())
.status() .status()
.map(|s| s.success()) .is_ok_and(|s| s.success());
.unwrap_or(false);
map.insert(name, running); map.insert(name, running);
} }
map map
@@ -183,7 +182,7 @@ fn update(state: &mut Dashboard, msg: Message) -> Task<Message> {
Message::PollDone(snapshot) => { Message::PollDone(snapshot) => {
state.running = snapshot; state.running = snapshot;
// Clear launch_busy for launchers that are now running // Clear launch_busy for launchers that are now running
state.launch_busy.retain(|n| !state.running.get(n).copied().unwrap_or(false)); state.launch_busy.retain(|n| !state.running.get(n).copied().unwrap_or_default());
Task::none() Task::none()
} }
Message::ReloadConfig => { Message::ReloadConfig => {
@@ -237,7 +236,7 @@ fn update(state: &mut Dashboard, msg: Message) -> Task<Message> {
let name2 = name.clone(); let name2 = name.clone();
state.running.insert(name, false); state.running.insert(name, false);
Task::perform( Task::perform(
async_blocking(move || launcher::kill(&l).map_err(|e| e.to_string())), async_blocking(move || { launcher::kill(&l); Ok::<(), String>(()) }),
move |res| Message::KillDone(name2.clone(), res), move |res| Message::KillDone(name2.clone(), res),
) )
} }
@@ -700,8 +699,7 @@ fn toggle_flag(
fn service_is_installed() -> bool { fn service_is_installed() -> bool {
dirs::home_dir() dirs::home_dir()
.map(|h| h.join(".config/autostart/umutray.desktop").exists()) .is_some_and(|h| h.join(".config/autostart/umutray.desktop").exists())
.unwrap_or(false)
} }
fn subscription(_: &Dashboard) -> Subscription<Message> { fn subscription(_: &Dashboard) -> Subscription<Message> {
@@ -1054,7 +1052,7 @@ fn launcher_card<'a>(
// ── Games section ───────────────────────────────────────────────────── // ── Games section ─────────────────────────────────────────────────────
let has_games = !l.games.is_empty(); let has_games = !l.games.is_empty();
let has_scan = scan_results.map(|r| !r.is_empty()).unwrap_or(false); let has_scan = scan_results.is_some_and(|r| !r.is_empty());
if has_games || has_scan || scan_busy { if has_games || has_scan || scan_busy {
let game_count = l.games.len(); let game_count = l.games.len();
+7 -10
View File
@@ -25,9 +25,9 @@ pub fn launch(config: &Config, launcher: &Launcher) -> Result<()> {
let exe = launcher.full_exe_path(); let exe = launcher.full_exe_path();
if !exe.exists() { if !exe.exists() {
bail!( bail!(
"launcher exe not found at {:?}\n\ "launcher exe not found at {}\n\
Run `umutray setup {}` for setup instructions.", Run `umutray setup {}` for setup instructions.",
exe, exe.display(),
launcher.name, launcher.name,
); );
} }
@@ -55,9 +55,9 @@ pub fn play_game(config: &Config, launcher: &Launcher, game: &Game) -> Result<()
let exe = game.full_exe_path(launcher); let exe = game.full_exe_path(launcher);
if !exe.exists() { if !exe.exists() {
bail!( bail!(
"game exe not found at {:?}\n\ "game exe not found at {}\n\
Check exe_path for '{}/{}' in config, or install the game via the launcher first.", Check exe_path for '{}/{}' in config, or install the game via the launcher first.",
exe, exe.display(),
launcher.name, launcher.name,
game.name, game.name,
); );
@@ -108,13 +108,12 @@ fn build_wrapped_argv(exe: &Path, game: &Game) -> (OsString, Vec<OsString>) {
} }
/// SIGTERM → wait 3 s → SIGKILL for a single launcher. /// SIGTERM → wait 3 s → SIGKILL for a single launcher.
pub fn kill(launcher: &Launcher) -> Result<()> { pub fn kill(launcher: &Launcher) {
kill_pattern(&launcher.process_pattern); kill_pattern(&launcher.process_pattern);
Ok(())
} }
/// Kill every configured launcher's processes. /// Kill every configured launcher's processes.
pub fn kill_all(config: &Config) -> Result<()> { pub fn kill_all(config: &Config) {
// Single SIGTERM pass across all launchers, then one sleep, then SIGKILL. // Single SIGTERM pass across all launchers, then one sleep, then SIGKILL.
// This keeps the total wait at 3 s instead of 3 s × N. // This keeps the total wait at 3 s instead of 3 s × N.
for l in &config.launchers { for l in &config.launchers {
@@ -124,7 +123,6 @@ pub fn kill_all(config: &Config) -> Result<()> {
for l in &config.launchers { for l in &config.launchers {
send_signal("-9", &l.process_pattern); send_signal("-9", &l.process_pattern);
} }
Ok(())
} }
fn kill_pattern(pattern: &str) { fn kill_pattern(pattern: &str) {
@@ -149,6 +147,5 @@ pub fn is_running(launcher: &Launcher) -> bool {
.stdout(Stdio::null()) .stdout(Stdio::null())
.stderr(Stdio::null()) .stderr(Stdio::null())
.status() .status()
.map(|s| s.success()) .is_ok_and(|s| s.success())
.unwrap_or(false)
} }
+8 -9
View File
@@ -254,9 +254,9 @@ fn main() -> Result<()> {
let l = config.find(&n).ok_or_else(|| { let l = config.find(&n).ok_or_else(|| {
anyhow::anyhow!("unknown launcher '{n}' — try `umutray launchers`") anyhow::anyhow!("unknown launcher '{n}' — try `umutray launchers`")
})?; })?;
launcher::kill(l)?; launcher::kill(l);
} }
None => launcher::kill_all(&config)?, None => launcher::kill_all(&config),
}, },
Commands::Diagnose { name } => { Commands::Diagnose { name } => {
@@ -414,15 +414,14 @@ fn main() -> Result<()> {
gamescope, gamescope,
no_gamescope, no_gamescope,
} => { } => {
// gs_update is Option<Option<Vec<String>>> where:
// None = leave gamescope unchanged
// Some(None) = disable gamescope
// Some(Some(args)) = enable gamescope with these CLI args
let gs_update = if no_gamescope { let gs_update = if no_gamescope {
Some(None) config::GamescopeUpdate::Disable
} else if let Some(s) = gamescope {
config::GamescopeUpdate::Enable(
s.split_whitespace().map(String::from).collect(),
)
} else { } else {
gamescope config::GamescopeUpdate::Unchanged
.map(|s| Some(s.split_whitespace().map(String::from).collect::<Vec<_>>()))
}; };
let mut c = config; let mut c = config;
c.set_game_flags(&launcher, &name, gamemode, mangohud, gs_update)?; c.set_game_flags(&launcher, &name, gamemode, mangohud, gs_update)?;
+6 -6
View File
@@ -4,7 +4,7 @@ use owo_colors::OwoColorize;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashSet; use std::collections::HashSet;
use std::io::Write; use std::io::Write;
use std::path::PathBuf; use std::path::{Path, PathBuf};
const GITHUB_API: &str = "https://api.github.com/repos/GloriousEggroll/proton-ge-custom/releases"; const GITHUB_API: &str = "https://api.github.com/repos/GloriousEggroll/proton-ge-custom/releases";
@@ -57,7 +57,7 @@ fn fetch_release(tag: &str) -> Result<Release> {
fn install_version(config: &Config, tag: &str) -> Result<()> { fn install_version(config: &Config, tag: &str) -> Result<()> {
let install_path = config.proton_compat_dir.join(tag); let install_path = config.proton_compat_dir.join(tag);
if install_path.exists() { if install_path.exists() {
println!("{tag} is already installed at {install_path:?}"); println!("{tag} is already installed at {}", install_path.display());
return Ok(()); return Ok(());
} }
@@ -83,13 +83,13 @@ fn install_version(config: &Config, tag: &str) -> Result<()> {
.context("Download returned an error status")?; .context("Download returned an error status")?;
let total = resp.content_length(); let total = resp.content_length();
let f = std::fs::File::create(&tmp_path) let f = std::fs::File::create(&tmp_path)
.with_context(|| format!("Failed to create temp file {tmp_path:?}"))?; .with_context(|| format!("Failed to create temp file {}", tmp_path.display()))?;
let mut progress = ProgressWriter::new(f, total); let mut progress = ProgressWriter::new(f, total);
std::io::copy(&mut resp, &mut progress).context("Failed to stream archive to disk")?; std::io::copy(&mut resp, &mut progress).context("Failed to stream archive to disk")?;
progress.finish(); progress.finish();
} }
println!("Extracting to {:?}...", config.proton_compat_dir); println!("Extracting to {}...", config.proton_compat_dir.display());
std::fs::create_dir_all(&config.proton_compat_dir)?; std::fs::create_dir_all(&config.proton_compat_dir)?;
let status = std::process::Command::new("tar") let status = std::process::Command::new("tar")
@@ -110,10 +110,10 @@ fn install_version(config: &Config, tag: &str) -> Result<()> {
} }
/// Return all GE-Proton* directories found in `dir`. /// Return all GE-Proton* directories found in `dir`.
fn scan_ge_proton_in(dir: &PathBuf, seen: &mut HashSet<String>, out: &mut Vec<String>) { fn scan_ge_proton_in(dir: &Path, seen: &mut HashSet<String>, out: &mut Vec<String>) {
let Ok(entries) = std::fs::read_dir(dir) else { return }; let Ok(entries) = std::fs::read_dir(dir) else { return };
for entry in entries.flatten() { for entry in entries.flatten() {
if !entry.file_type().map(|t| t.is_dir()).unwrap_or(false) { if !entry.file_type().is_ok_and(|t| t.is_dir()) {
continue; continue;
} }
let name = entry.file_name().to_string_lossy().to_string(); let name = entry.file_name().to_string_lossy().to_string();
+12 -13
View File
@@ -17,27 +17,26 @@ fn desktop_path() -> Result<PathBuf> {
} }
fn render_desktop(exe: &std::path::Path, autostart: bool) -> String { fn render_desktop(exe: &std::path::Path, autostart: bool) -> String {
let exec = if autostart {
format!("{}", exe.display())
} else {
format!("{} gui", exe.display())
};
let mut s = format!( let mut s = format!(
"[Desktop Entry]\n\ "[Desktop Entry]\n\
Name=umutray\n\ Name=umutray\n\
Comment=Wine launcher manager for Windows game launchers\n\ Comment=Wine launcher manager for Windows game launchers\n\
Exec={exe}\n\ Exec={exec}\n\
Icon=applications-games\n\ Icon=applications-games\n\
Type=Application\n\ Type=Application\n\
Categories=Game;\n\ Categories=Game;\n\
Keywords=wine;proton;gaming;launcher;\n\ Keywords=wine;proton;gaming;launcher;\n\
StartupNotify=false\n", StartupNotify=false\n",
exe = exe.display(),
); );
if autostart { if autostart {
s.push_str("X-GNOME-Autostart-enabled=true\n"); s.push_str("X-GNOME-Autostart-enabled=true\n");
s.push_str("Hidden=false\n"); s.push_str("Hidden=false\n");
} else { } else {
// App-menu entry launches the GUI
s = s.replace(
&format!("Exec={}", exe.display()),
&format!("Exec={} gui", exe.display()),
);
s.push_str("StartupNotify=true\n"); s.push_str("StartupNotify=true\n");
} }
s s
@@ -49,10 +48,10 @@ pub fn install_desktop() -> Result<()> {
let exe = std::env::current_exe().context("Cannot determine path to own executable")?; let exe = std::env::current_exe().context("Cannot determine path to own executable")?;
let desktop = desktop_path()?; let desktop = desktop_path()?;
if let Some(p) = desktop.parent() { if let Some(p) = desktop.parent() {
std::fs::create_dir_all(p).with_context(|| format!("Failed to create {p:?}"))?; std::fs::create_dir_all(p).with_context(|| format!("Failed to create {}", p.display()))?;
} }
std::fs::write(&desktop, render_desktop(&exe, false)) std::fs::write(&desktop, render_desktop(&exe, false))
.with_context(|| format!("Failed to write desktop file {desktop:?}"))?; .with_context(|| format!("Failed to write desktop file {}", desktop.display()))?;
println!("{} App menu entry written: {}", "".green().bold(), desktop.display()); println!("{} App menu entry written: {}", "".green().bold(), desktop.display());
Ok(()) Ok(())
} }
@@ -62,7 +61,7 @@ pub fn uninstall_desktop() -> Result<()> {
let desktop = desktop_path()?; let desktop = desktop_path()?;
if desktop.exists() { if desktop.exists() {
std::fs::remove_file(&desktop) std::fs::remove_file(&desktop)
.with_context(|| format!("Failed to remove {desktop:?}"))?; .with_context(|| format!("Failed to remove {}", desktop.display()))?;
println!("Removed {}", desktop.display()); println!("Removed {}", desktop.display());
} else { } else {
println!("No desktop file at {}", desktop.display()); println!("No desktop file at {}", desktop.display());
@@ -77,10 +76,10 @@ pub fn install() -> Result<()> {
// XDG autostart // XDG autostart
let autostart = autostart_path()?; let autostart = autostart_path()?;
if let Some(p) = autostart.parent() { if let Some(p) = autostart.parent() {
std::fs::create_dir_all(p).with_context(|| format!("Failed to create {p:?}"))?; std::fs::create_dir_all(p).with_context(|| format!("Failed to create {}", p.display()))?;
} }
std::fs::write(&autostart, render_desktop(&exe, true)) std::fs::write(&autostart, render_desktop(&exe, true))
.with_context(|| format!("Failed to write autostart file {autostart:?}"))?; .with_context(|| format!("Failed to write autostart file {}", autostart.display()))?;
println!("Wrote autostart: {}", autostart.display()); println!("Wrote autostart: {}", autostart.display());
// App-menu entry // App-menu entry
@@ -98,7 +97,7 @@ pub fn uninstall() -> Result<()> {
let autostart = autostart_path()?; let autostart = autostart_path()?;
if autostart.exists() { if autostart.exists() {
std::fs::remove_file(&autostart) std::fs::remove_file(&autostart)
.with_context(|| format!("Failed to remove {autostart:?}"))?; .with_context(|| format!("Failed to remove {}", autostart.display()))?;
println!("Removed {}", autostart.display()); println!("Removed {}", autostart.display());
} else { } else {
println!("No autostart file at {}", autostart.display()); println!("No autostart file at {}", autostart.display());
+1 -1
View File
@@ -144,7 +144,7 @@ impl ksni::Tray for UmuTray {
icon_name: "process-stop".into(), icon_name: "process-stop".into(),
activate: Box::new(move |this: &mut Self| { activate: Box::new(move |this: &mut Self| {
if let Some(l) = this.config.find(&kill_name) { if let Some(l) = this.config.find(&kill_name) {
let _ = launcher::kill(l); launcher::kill(l);
} }
}), }),
..Default::default() ..Default::default()
+4 -8
View File
@@ -1,6 +1,4 @@
use iced::futures::channel::oneshot; /// Run a blocking closure on the tokio blocking thread pool and await its result.
/// Run a blocking closure on a thread pool thread and await its result.
/// Used to offload blocking work (HTTP, disk, process spawning) without /// Used to offload blocking work (HTTP, disk, process spawning) without
/// stalling the iced event loop. /// stalling the iced event loop.
pub async fn async_blocking<T, F>(f: F) -> T pub async fn async_blocking<T, F>(f: F) -> T
@@ -8,11 +6,9 @@ where
T: Send + 'static, T: Send + 'static,
F: FnOnce() -> T + Send + 'static, F: FnOnce() -> T + Send + 'static,
{ {
let (tx, rx) = oneshot::channel(); tokio::task::spawn_blocking(f)
std::thread::spawn(move || { .await
let _ = tx.send(f()); .expect("blocking task panicked")
});
rx.await.expect("blocking task panicked")
} }
/// Open a native folder picker dialog and return the chosen path, or None if /// Open a native folder picker dialog and return the chosen path, or None if