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:
Generated
+1
@@ -4439,6 +4439,7 @@ dependencies = [
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"toml",
|
||||
]
|
||||
|
||||
|
||||
@@ -45,3 +45,4 @@ reqwest = { version = "0.12", features = ["blocking", "json"] }
|
||||
# GUI for the setup wizard
|
||||
iced = { version = "0.13", features = ["tokio"] }
|
||||
iced_fonts = { version = "0.1", features = ["bootstrap"] }
|
||||
tokio = { version = "1.52.1", features = ["rt"] }
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
|
||||
pkgname=umutray
|
||||
pkgver=0.1.0
|
||||
pkgrel=4
|
||||
pkgrel=6
|
||||
pkgdesc='Tray-based Wine launcher manager for Linux via umu/Proton-GE'
|
||||
arch=('x86_64')
|
||||
url='https://git.aleshym.co/funman300/umutray'
|
||||
|
||||
+20
-8
@@ -4,6 +4,17 @@ use owo_colors::OwoColorize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
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)]
|
||||
pub struct Launcher {
|
||||
/// Short CLI name (e.g. "battlenet").
|
||||
@@ -236,7 +247,7 @@ impl Config {
|
||||
return Ok(c);
|
||||
}
|
||||
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) {
|
||||
Ok(c) => {
|
||||
Ok(c)
|
||||
@@ -244,7 +255,7 @@ impl Config {
|
||||
Err(e) => {
|
||||
let bak = path.with_extension("toml.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!(
|
||||
" backed up to {} — writing fresh config with presets",
|
||||
@@ -264,7 +275,7 @@ impl Config {
|
||||
}
|
||||
let content = toml::to_string_pretty(self)?;
|
||||
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> {
|
||||
@@ -386,16 +397,15 @@ impl Config {
|
||||
}
|
||||
|
||||
/// 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(
|
||||
&mut self,
|
||||
launcher: &str,
|
||||
name: &str,
|
||||
gamemode: Option<bool>,
|
||||
mangohud: Option<bool>,
|
||||
gamescope: Option<Option<Vec<String>>>,
|
||||
gamescope: GamescopeUpdate,
|
||||
) -> Result<()> {
|
||||
if gamemode.is_none() && mangohud.is_none() && gamescope.is_none() {
|
||||
if gamemode.is_none() && mangohud.is_none() && matches!(gamescope, GamescopeUpdate::Unchanged) {
|
||||
anyhow::bail!(
|
||||
"nothing to set — pass --gamemode, --mangohud, --gamescope, or --no-gamescope"
|
||||
);
|
||||
@@ -415,8 +425,10 @@ impl Config {
|
||||
if let Some(v) = mangohud {
|
||||
g.mangohud = v;
|
||||
}
|
||||
if let Some(v) = gamescope {
|
||||
g.gamescope = v;
|
||||
match gamescope {
|
||||
GamescopeUpdate::Unchanged => {}
|
||||
GamescopeUpdate::Disable => g.gamescope = None,
|
||||
GamescopeUpdate::Enable(args) => g.gamescope = Some(args),
|
||||
}
|
||||
self.save()?;
|
||||
println!("{} Updated flags for '{launcher}/{name}'.", "✓".green().bold());
|
||||
|
||||
+6
-21
@@ -23,8 +23,7 @@ pub fn scan_for_gui(config: &Config) -> Vec<DetectHit> {
|
||||
if prefix.join("drive_c").join(&preset.exe_path).exists() {
|
||||
let configured = config
|
||||
.find(&preset.name)
|
||||
.map(|l| l.prefix_dir == *prefix)
|
||||
.unwrap_or(false);
|
||||
.is_some_and(|l| l.prefix_dir == *prefix);
|
||||
hits.push(DetectHit {
|
||||
display: preset.display.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`.
|
||||
fn store_title(exe_path: &Path) -> Option<String> {
|
||||
let mut dir = exe_path.parent();
|
||||
while let Some(d) = dir {
|
||||
if let Some(title) = STORE_TITLES.get(d) {
|
||||
return Some(title.clone());
|
||||
}
|
||||
dir = d.parent();
|
||||
}
|
||||
None
|
||||
exe_path.ancestors().skip(1).find_map(|d| STORE_TITLES.get(d).cloned())
|
||||
}
|
||||
|
||||
/// Scan a launcher's Wine prefix for installed game executables.
|
||||
@@ -251,8 +243,7 @@ fn scan_exe_dir(
|
||||
} else if path
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|e| e.eq_ignore_ascii_case("exe"))
|
||||
.unwrap_or(false)
|
||||
.is_some_and(|e| e.eq_ignore_ascii_case("exe"))
|
||||
{
|
||||
let Ok(rel) = path.strip_prefix(drive_c) else { continue };
|
||||
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": "..." }`
|
||||
/// - Epic: `.egstore/<id>.item` → `{ "DisplayName": "..." }`
|
||||
fn read_manifest_name(exe_path: &Path) -> Option<String> {
|
||||
let mut dir = exe_path.parent();
|
||||
while let Some(d) = dir {
|
||||
for d in exe_path.ancestors().skip(1) {
|
||||
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
|
||||
// are never above the game's installation folder.
|
||||
if dirname == "drive_c" || dirname.starts_with("program files") {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(name) = read_gog_manifest(d).or_else(|| read_epic_manifest(d)) {
|
||||
return Some(name);
|
||||
}
|
||||
|
||||
dir = d.parent();
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -438,8 +425,7 @@ fn nearest_dir_name(path: &Path) -> String {
|
||||
"launcher", "engine", "client",
|
||||
];
|
||||
|
||||
let mut dir = path.parent();
|
||||
while let Some(d) = dir {
|
||||
for d in path.ancestors().skip(1) {
|
||||
let name = d.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||
let lower = name.to_lowercase();
|
||||
if !name.is_empty()
|
||||
@@ -448,7 +434,6 @@ fn nearest_dir_name(path: &Path) -> String {
|
||||
{
|
||||
return name.to_string();
|
||||
}
|
||||
dir = d.parent();
|
||||
}
|
||||
|
||||
// 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;
|
||||
};
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
+14
-14
@@ -80,8 +80,7 @@ fn global_vulkan_check() -> CheckResult {
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false);
|
||||
.is_ok_and(|s| s.success());
|
||||
if ok {
|
||||
CheckResult::pass("vulkan", "vulkaninfo OK")
|
||||
} else {
|
||||
@@ -218,8 +217,7 @@ fn count_ge_proton(dir: &Path) -> usize {
|
||||
.filter(|e| {
|
||||
e.file_name()
|
||||
.to_str()
|
||||
.map(|s| s.starts_with("GE-Proton"))
|
||||
.unwrap_or(false)
|
||||
.is_some_and(|s| s.starts_with("GE-Proton"))
|
||||
})
|
||||
.count()
|
||||
})
|
||||
@@ -236,19 +234,21 @@ fn which(cmd: &str) -> Option<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 {
|
||||
let file_uid = match std::fs::metadata(path) {
|
||||
Ok(m) => m.uid(),
|
||||
Err(_) => return true,
|
||||
};
|
||||
let current_uid: Option<u32> = Command::new("id")
|
||||
.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,
|
||||
}
|
||||
current_uid().map_or(true, |uid| uid == file_uid)
|
||||
}
|
||||
|
||||
+5
-7
@@ -171,8 +171,7 @@ fn update(state: &mut Dashboard, msg: Message) -> Task<Message> {
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false);
|
||||
.is_ok_and(|s| s.success());
|
||||
map.insert(name, running);
|
||||
}
|
||||
map
|
||||
@@ -183,7 +182,7 @@ fn update(state: &mut Dashboard, msg: Message) -> Task<Message> {
|
||||
Message::PollDone(snapshot) => {
|
||||
state.running = snapshot;
|
||||
// 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()
|
||||
}
|
||||
Message::ReloadConfig => {
|
||||
@@ -237,7 +236,7 @@ fn update(state: &mut Dashboard, msg: Message) -> Task<Message> {
|
||||
let name2 = name.clone();
|
||||
state.running.insert(name, false);
|
||||
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),
|
||||
)
|
||||
}
|
||||
@@ -700,8 +699,7 @@ fn toggle_flag(
|
||||
|
||||
fn service_is_installed() -> bool {
|
||||
dirs::home_dir()
|
||||
.map(|h| h.join(".config/autostart/umutray.desktop").exists())
|
||||
.unwrap_or(false)
|
||||
.is_some_and(|h| h.join(".config/autostart/umutray.desktop").exists())
|
||||
}
|
||||
|
||||
fn subscription(_: &Dashboard) -> Subscription<Message> {
|
||||
@@ -1054,7 +1052,7 @@ fn launcher_card<'a>(
|
||||
|
||||
// ── Games section ─────────────────────────────────────────────────────
|
||||
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 {
|
||||
let game_count = l.games.len();
|
||||
|
||||
+7
-10
@@ -25,9 +25,9 @@ pub fn launch(config: &Config, launcher: &Launcher) -> Result<()> {
|
||||
let exe = launcher.full_exe_path();
|
||||
if !exe.exists() {
|
||||
bail!(
|
||||
"launcher exe not found at {:?}\n\
|
||||
"launcher exe not found at {}\n\
|
||||
Run `umutray setup {}` for setup instructions.",
|
||||
exe,
|
||||
exe.display(),
|
||||
launcher.name,
|
||||
);
|
||||
}
|
||||
@@ -55,9 +55,9 @@ pub fn play_game(config: &Config, launcher: &Launcher, game: &Game) -> Result<()
|
||||
let exe = game.full_exe_path(launcher);
|
||||
if !exe.exists() {
|
||||
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.",
|
||||
exe,
|
||||
exe.display(),
|
||||
launcher.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.
|
||||
pub fn kill(launcher: &Launcher) -> Result<()> {
|
||||
pub fn kill(launcher: &Launcher) {
|
||||
kill_pattern(&launcher.process_pattern);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
// This keeps the total wait at 3 s instead of 3 s × N.
|
||||
for l in &config.launchers {
|
||||
@@ -124,7 +123,6 @@ pub fn kill_all(config: &Config) -> Result<()> {
|
||||
for l in &config.launchers {
|
||||
send_signal("-9", &l.process_pattern);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn kill_pattern(pattern: &str) {
|
||||
@@ -149,6 +147,5 @@ pub fn is_running(launcher: &Launcher) -> bool {
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false)
|
||||
.is_ok_and(|s| s.success())
|
||||
}
|
||||
|
||||
+8
-9
@@ -254,9 +254,9 @@ fn main() -> Result<()> {
|
||||
let l = config.find(&n).ok_or_else(|| {
|
||||
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 } => {
|
||||
@@ -414,15 +414,14 @@ fn main() -> Result<()> {
|
||||
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 {
|
||||
Some(None)
|
||||
config::GamescopeUpdate::Disable
|
||||
} else if let Some(s) = gamescope {
|
||||
config::GamescopeUpdate::Enable(
|
||||
s.split_whitespace().map(String::from).collect(),
|
||||
)
|
||||
} else {
|
||||
gamescope
|
||||
.map(|s| Some(s.split_whitespace().map(String::from).collect::<Vec<_>>()))
|
||||
config::GamescopeUpdate::Unchanged
|
||||
};
|
||||
let mut c = config;
|
||||
c.set_game_flags(&launcher, &name, gamemode, mangohud, gs_update)?;
|
||||
|
||||
+6
-6
@@ -4,7 +4,7 @@ use owo_colors::OwoColorize;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashSet;
|
||||
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";
|
||||
|
||||
@@ -57,7 +57,7 @@ fn fetch_release(tag: &str) -> Result<Release> {
|
||||
fn install_version(config: &Config, tag: &str) -> Result<()> {
|
||||
let install_path = config.proton_compat_dir.join(tag);
|
||||
if install_path.exists() {
|
||||
println!("{tag} is already installed at {install_path:?}");
|
||||
println!("{tag} is already installed at {}", install_path.display());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -83,13 +83,13 @@ fn install_version(config: &Config, tag: &str) -> Result<()> {
|
||||
.context("Download returned an error status")?;
|
||||
let total = resp.content_length();
|
||||
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);
|
||||
std::io::copy(&mut resp, &mut progress).context("Failed to stream archive to disk")?;
|
||||
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)?;
|
||||
|
||||
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`.
|
||||
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 };
|
||||
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;
|
||||
}
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
|
||||
+12
-13
@@ -17,27 +17,26 @@ fn desktop_path() -> Result<PathBuf> {
|
||||
}
|
||||
|
||||
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!(
|
||||
"[Desktop Entry]\n\
|
||||
Name=umutray\n\
|
||||
Comment=Wine launcher manager for Windows game launchers\n\
|
||||
Exec={exe}\n\
|
||||
Exec={exec}\n\
|
||||
Icon=applications-games\n\
|
||||
Type=Application\n\
|
||||
Categories=Game;\n\
|
||||
Keywords=wine;proton;gaming;launcher;\n\
|
||||
StartupNotify=false\n",
|
||||
exe = exe.display(),
|
||||
);
|
||||
if autostart {
|
||||
s.push_str("X-GNOME-Autostart-enabled=true\n");
|
||||
s.push_str("Hidden=false\n");
|
||||
} 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
|
||||
@@ -49,10 +48,10 @@ pub fn install_desktop() -> Result<()> {
|
||||
let exe = std::env::current_exe().context("Cannot determine path to own executable")?;
|
||||
let desktop = desktop_path()?;
|
||||
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))
|
||||
.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());
|
||||
Ok(())
|
||||
}
|
||||
@@ -62,7 +61,7 @@ pub fn uninstall_desktop() -> Result<()> {
|
||||
let desktop = desktop_path()?;
|
||||
if desktop.exists() {
|
||||
std::fs::remove_file(&desktop)
|
||||
.with_context(|| format!("Failed to remove {desktop:?}"))?;
|
||||
.with_context(|| format!("Failed to remove {}", desktop.display()))?;
|
||||
println!("Removed {}", desktop.display());
|
||||
} else {
|
||||
println!("No desktop file at {}", desktop.display());
|
||||
@@ -77,10 +76,10 @@ pub fn install() -> Result<()> {
|
||||
// XDG autostart
|
||||
let autostart = autostart_path()?;
|
||||
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))
|
||||
.with_context(|| format!("Failed to write autostart file {autostart:?}"))?;
|
||||
.with_context(|| format!("Failed to write autostart file {}", autostart.display()))?;
|
||||
println!("Wrote autostart: {}", autostart.display());
|
||||
|
||||
// App-menu entry
|
||||
@@ -98,7 +97,7 @@ pub fn uninstall() -> Result<()> {
|
||||
let autostart = autostart_path()?;
|
||||
if autostart.exists() {
|
||||
std::fs::remove_file(&autostart)
|
||||
.with_context(|| format!("Failed to remove {autostart:?}"))?;
|
||||
.with_context(|| format!("Failed to remove {}", autostart.display()))?;
|
||||
println!("Removed {}", autostart.display());
|
||||
} else {
|
||||
println!("No autostart file at {}", autostart.display());
|
||||
|
||||
+1
-1
@@ -144,7 +144,7 @@ impl ksni::Tray for UmuTray {
|
||||
icon_name: "process-stop".into(),
|
||||
activate: Box::new(move |this: &mut Self| {
|
||||
if let Some(l) = this.config.find(&kill_name) {
|
||||
let _ = launcher::kill(l);
|
||||
launcher::kill(l);
|
||||
}
|
||||
}),
|
||||
..Default::default()
|
||||
|
||||
+4
-8
@@ -1,6 +1,4 @@
|
||||
use iced::futures::channel::oneshot;
|
||||
|
||||
/// Run a blocking closure on a thread pool thread and await its result.
|
||||
/// Run a blocking closure on the tokio blocking thread pool and await its result.
|
||||
/// Used to offload blocking work (HTTP, disk, process spawning) without
|
||||
/// stalling the iced event loop.
|
||||
pub async fn async_blocking<T, F>(f: F) -> T
|
||||
@@ -8,11 +6,9 @@ where
|
||||
T: Send + 'static,
|
||||
F: FnOnce() -> T + Send + 'static,
|
||||
{
|
||||
let (tx, rx) = oneshot::channel();
|
||||
std::thread::spawn(move || {
|
||||
let _ = tx.send(f());
|
||||
});
|
||||
rx.await.expect("blocking task panicked")
|
||||
tokio::task::spawn_blocking(f)
|
||||
.await
|
||||
.expect("blocking task panicked")
|
||||
}
|
||||
|
||||
/// Open a native folder picker dialog and return the chosen path, or None if
|
||||
|
||||
Reference in New Issue
Block a user