fix: update all dependencies, remove pelite and directories crates

- Update iced 0.13 -> 0.14 (API migration: application builder, theme, font loading)
- Update ksni 0.2 -> 0.3 (blocking API)
- Update dirs 5 -> 6, toml 0.8 -> 1, reqwest 0.12 -> 0.13, rfd 0.15 -> 0.17, iced_fonts 0.1 -> 0.3
- Remove pelite dependency (PE file parsing was unreliable and unnecessary)
- Remove directories dependency (consolidated with dirs crate)

Closes #8, #9, #10
This commit is contained in:
funman300
2026-04-19 15:30:46 -07:00
parent c1893f9f64
commit 4845ebe4f8
8 changed files with 1056 additions and 1565 deletions
Generated
+933 -1426
View File
File diff suppressed because it is too large Load Diff
+8 -14
View File
@@ -19,7 +19,7 @@ clap = { version = "4", features = ["derive"] }
# Config serialisation
serde = { version = "1", features = ["derive"] }
toml = "0.8"
toml = "1"
# GitHub API responses
serde_json = "1"
@@ -27,28 +27,22 @@ serde_json = "1"
# Error handling
anyhow = "1"
# XDG config / data paths
directories = "5"
# Home directory lookup
dirs = "5"
# XDG config / data / home paths
dirs = "6"
# Terminal colour output
owo-colors = "4"
# System tray via D-Bus StatusNotifierItem (KDE, GNOME+AppIndicator, Xfce, etc.)
ksni = "0.2"
ksni = { version = "0.3", features = ["blocking"] }
# HTTP for GE-Proton GitHub releases API
reqwest = { version = "0.12", features = ["blocking", "json"] }
reqwest = { version = "0.13", features = ["blocking", "json"] }
# GUI for the setup wizard
iced = { version = "0.13", features = ["tokio"] }
iced_fonts = { version = "0.1", features = ["bootstrap"] }
iced = { version = "0.14", features = ["tokio"] }
iced_fonts = { version = "0.3", features = ["bootstrap"] }
tokio = { version = "1.52.1", features = ["rt"] }
# PE exe version info (game name detection)
pelite = "0.10"
# Native file dialogs via XDG Desktop Portal
rfd = "0.15"
rfd = "0.17"
+2 -3
View File
@@ -1,5 +1,4 @@
use anyhow::{Context, Result};
use directories::ProjectDirs;
use owo_colors::OwoColorize;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
@@ -234,9 +233,9 @@ impl Default for Config {
impl Config {
pub fn config_path() -> Result<PathBuf> {
let dirs = ProjectDirs::from("co.aleshym", "", "umutray")
let config_dir = dirs::config_dir()
.context("Could not determine config directory")?;
Ok(dirs.config_dir().join("config.toml"))
Ok(config_dir.join("umutray").join("config.toml"))
}
pub fn load() -> Result<Self> {
+2 -38
View File
@@ -1,8 +1,6 @@
use crate::config::{Config, Launcher};
use anyhow::Result;
use owo_colors::OwoColorize;
use pelite::pe64::Pe as _;
use pelite::pe32::Pe as _;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::{LazyLock, Mutex};
@@ -280,9 +278,7 @@ fn scan_exe_dir(
/// Epic `.egstore/*.item` JSON files at the game's installation root.
/// 4. Launcher path — reads the game name from well-known directory
/// structures laid down by the launcher (e.g. `Epic Games/<Name>/`).
/// 5. PE version info — reads `ProductName` or `FileDescription` from
/// the exe's embedded Windows version resource.
/// 6. Nearest non-generic parent directory name, or raw exe stem.
/// 5. Nearest non-generic parent directory name, or raw exe stem.
/// No name generation — if the directory name is unknown, it is used
/// as-is rather than being fabricated from the exe filename.
///
@@ -325,12 +321,7 @@ fn resolve_uncached(exe_path: &Path) -> String {
return name;
}
// Stage 5 PE version info embedded in the exe itself
if let Some(name) = read_pe_product_name(exe_path) {
return name;
}
// Stage 6 nearest non-generic parent directory, or raw exe stem.
// Stage 5 nearest non-generic parent directory, or raw exe stem.
// No name generation: if we don't know, we say so honestly.
prettify_exe_name(exe_path)
}
@@ -426,33 +417,6 @@ fn name_from_launcher_path(exe_path: &Path) -> Option<String> {
None
}
/// Read the `ProductName` (preferred) or `FileDescription` from the exe's
/// embedded PE version info resource. This is the same data Windows uses for
/// "Properties → Details" and is present in the vast majority of game exes.
fn read_pe_product_name(exe_path: &Path) -> Option<String> {
let map = pelite::FileMap::open(exe_path).ok()?;
// Try 64-bit first, fall back to 32-bit.
let vi = pelite::pe64::PeFile::from_bytes(&map)
.ok()
.and_then(|pe| pe.resources().ok()?.version_info().ok())
.or_else(|| {
pelite::pe32::PeFile::from_bytes(&map)
.ok()?
.resources().ok()?
.version_info().ok()
})?;
let lang = *vi.translation().first()?;
// ProductName is the canonical game title; FileDescription is a fallback.
let name = vi
.value(lang, "ProductName")
.or_else(|| vi.value(lang, "FileDescription"))?;
let name = name.trim();
if name.is_empty() {
return None;
}
Some(name.to_string())
}
/// Heuristic last-resort name derivation from an exe path.
///
/// Walks up parent directories looking for a non-generic name; falls back to
+49 -37
View File
@@ -684,13 +684,13 @@ fn update(state: &mut Dashboard, msg: Message) -> Task<Message> {
if let Ok(mut a) = state.close_action.lock() {
*a = Some(CloseAction::Quit);
}
iced::window::get_oldest().and_then(iced::window::close)
iced::window::oldest().and_then(iced::window::close)
}
Message::ConfirmMinimize => {
if let Ok(mut a) = state.close_action.lock() {
*a = Some(CloseAction::MinimizeToTray);
}
iced::window::get_oldest().and_then(iced::window::close)
iced::window::oldest().and_then(iced::window::close)
}
Message::CancelClose => {
state.close_dialog_open = false;
@@ -728,7 +728,13 @@ fn subscription(_: &Dashboard) -> Subscription<Message> {
Subscription::batch([
iced::time::every(Duration::from_secs(2)).map(|_| Message::PollProcesses),
iced::time::every(Duration::from_secs(5)).map(|_| Message::ReloadConfig),
iced::window::close_requests().map(Message::CloseRequested),
iced::event::listen_with(|event, _status, id| {
if let iced::Event::Window(iced::window::Event::CloseRequested) = event {
Some(Message::CloseRequested(id))
} else {
None
}
}),
])
}
@@ -761,7 +767,7 @@ fn view(state: &Dashboard) -> Element<'_, Message> {
color: Some(DIM),
}),
].spacing(2),
iced::widget::horizontal_space(),
Space::new().width(Length::Fill),
settings_btn,
]
.align_y(Alignment::Center),
@@ -769,7 +775,7 @@ fn view(state: &Dashboard) -> Element<'_, Message> {
.padding(Padding { top: 20.0, right: 24.0, bottom: 6.0, left: 24.0 });
// ── Accent bar under title ────────────────────────────────────────────
let accent_bar = container(Space::new(0, 0))
let accent_bar = container(Space::new())
.width(Length::Fill)
.height(Length::Fixed(2.0))
.style(|_: &Theme| container::Style {
@@ -792,14 +798,14 @@ fn view(state: &Dashboard) -> Element<'_, Message> {
accent_bar,
container(
column![
Space::new(0, 40),
Space::new().height(40),
text("No launchers configured").size(16).style(|_: &Theme| text::Style {
color: Some(DIM),
}),
text("Add a launcher to get started.").size(12).style(|_: &Theme| text::Style {
color: Some(MUTED),
}),
Space::new(0, 12),
Space::new().height(12),
add_btn,
]
.spacing(6)
@@ -893,7 +899,7 @@ fn view(state: &Dashboard) -> Element<'_, Message> {
detect_btn,
proton_btn,
add_btn,
iced::widget::horizontal_space(),
Space::new().width(Length::Fill),
footer_status,
]
.align_y(Alignment::Center)
@@ -938,7 +944,7 @@ fn view(state: &Dashboard) -> Element<'_, Message> {
})
.into()
} else {
Space::new(0, 0).into()
Space::new().into()
};
let body = column![
@@ -1036,7 +1042,7 @@ fn launcher_card<'a>(
})
.into()
} else {
Space::new(0, 0).into()
Space::new().into()
};
// ── Status pill ───────────────────────────────────────────────────────
@@ -1065,7 +1071,7 @@ fn launcher_card<'a>(
status_pill,
].align_y(Alignment::Center),
].spacing(6),
iced::widget::horizontal_space(),
Space::new().width(Length::Fill),
action,
]
.align_y(Alignment::Center);
@@ -1095,7 +1101,7 @@ fn launcher_card<'a>(
let section_header = row![
text(section_label).size(10)
.style(move |_: &Theme| text::Style { color: Some(DIM) }),
iced::widget::horizontal_space(),
Space::new().width(Length::Fill),
rescan_btn,
]
.align_y(Alignment::Center);
@@ -1139,7 +1145,7 @@ fn launcher_card<'a>(
text(display).size(13),
text(exe).size(9).style(move |_: &Theme| text::Style { color: Some(MUTED) }),
].spacing(2),
iced::widget::horizontal_space(),
Space::new().width(Length::Fill),
add_btn,
]
.align_y(Alignment::Center)
@@ -1200,6 +1206,7 @@ fn launcher_card<'a>(
radius: 10.0.into(),
},
shadow: NO_SHADOW,
snap: false,
})
.padding([3, 9])
.into()
@@ -1222,7 +1229,7 @@ fn launcher_card<'a>(
play,
container(status_dot).padding(Padding { top: 0.0, left: 2.0, right: 2.0, bottom: 0.0 }),
text(&g.display).size(14),
iced::widget::horizontal_space(),
Space::new().width(Length::Fill),
overlays,
container(remove_game).padding(Padding { top: 0.0, left: 8.0, right: 0.0, bottom: 0.0 }),
]
@@ -1257,7 +1264,7 @@ fn launcher_card<'a>(
row![
text("No games configured").size(12)
.style(move |_: &Theme| text::Style { color: Some(MUTED) }),
iced::widget::horizontal_space(),
Space::new().width(Length::Fill),
scan_btn,
browse_btn,
]
@@ -1339,7 +1346,7 @@ fn launcher_card<'a>(
sections.push(
row![
add_game_btn,
iced::widget::horizontal_space(),
Space::new().width(Length::Fill),
scan_btn,
browse_btn,
]
@@ -1360,7 +1367,7 @@ fn context_menu_card(l: &crate::config::Launcher) -> Element<'_, Message> {
}),
]
.spacing(3),
iced::widget::horizontal_space(),
Space::new().width(Length::Fill),
button(icon("\u{f659}", 13))
.on_press(Message::HideContextMenu)
.style(btn_ghost)
@@ -1395,11 +1402,11 @@ fn context_menu_card(l: &crate::config::Launcher) -> Element<'_, Message> {
column![
header,
Space::new(0, 4),
Space::new().height(4),
menu_btn("Open install folder", "\u{f3e8}", Message::OpenPrefix(l.name.clone())),
menu_btn("Re-run setup wizard", "\u{f130}", Message::RerunSetup(l.name.clone())),
menu_btn("Run diagnostics", "\u{f52a}", Message::DiagnosePressed(l.name.clone())),
Space::new(0, 2),
Space::new().height(2),
remove,
]
.spacing(2)
@@ -1418,7 +1425,7 @@ fn diagnose_card<'a>(
}),
]
.spacing(3),
iced::widget::horizontal_space(),
Space::new().width(Length::Fill),
button(icon("\u{f659}", 13))
.on_press(Message::HideDiagnose)
.style(btn_ghost)
@@ -1452,7 +1459,7 @@ fn diagnose_card<'a>(
color: Some(color),
}),
text(format!(" {}", c.label)).size(12),
iced::widget::horizontal_space(),
Space::new().width(Length::Fill),
text(&c.detail).size(11).style(|_: &Theme| text::Style {
color: Some(DIM),
}),
@@ -1469,7 +1476,7 @@ fn diagnose_card<'a>(
column![
header,
Space::new(0, 6),
Space::new().height(6),
body,
]
.spacing(4)
@@ -1483,7 +1490,7 @@ fn settings_section<'a>(title: &str, content: Element<'a, Message>) -> Element<'
text(title.to_uppercase()).size(10).style(|_: &Theme| text::Style {
color: Some(DIM),
}),
Space::new(0, 4),
Space::new().height(4),
content,
]
.spacing(2),
@@ -1544,7 +1551,7 @@ fn view_close_dialog() -> Element<'static, Message> {
column![
title_row,
description,
Space::new(0, 8),
Space::new().height(8),
minimize_btn,
quit_btn,
cancel_btn,
@@ -1578,7 +1585,7 @@ fn view_settings(state: &Dashboard) -> Element<'_, Message> {
icon("\u{f3e2}", 20).style(|_: &Theme| text::Style { color: Some(ACCENT) }),
text(" Settings").size(22),
].align_y(Alignment::Center),
iced::widget::horizontal_space(),
Space::new().width(Length::Fill),
button(
row![icon("\u{f12f}", 13), text(" Back").size(12)]
.align_y(Alignment::Center).spacing(4),
@@ -1626,10 +1633,10 @@ fn view_settings(state: &Dashboard) -> Element<'_, Message> {
column![
text("Version").size(12).style(|_: &Theme| text::Style { color: Some(DIM) }),
proton_version_picker,
Space::new(0, 4),
Space::new().height(4),
text("Compat directory").size(12).style(|_: &Theme| text::Style { color: Some(DIM) }),
row![compat_dir_input, browse_compat_btn].spacing(8).align_y(Alignment::Center),
Space::new(0, 4),
Space::new().height(4),
container(save_btn).width(Length::Fill).align_x(Alignment::End),
].spacing(6).into(),
);
@@ -1677,7 +1684,7 @@ fn view_settings(state: &Dashboard) -> Element<'_, Message> {
text(format!(" {svc_status_text}")).size(12).style(move |_: &Theme| text::Style {
color: Some(svc_status_color),
}),
iced::widget::horizontal_space(),
Space::new().width(Length::Fill),
svc_install_btn,
svc_uninstall_btn,
].align_y(Alignment::Center).spacing(8),
@@ -1706,12 +1713,12 @@ fn view_settings(state: &Dashboard) -> Element<'_, Message> {
})
.into()
} else {
Space::new(0, 0).into()
Space::new().into()
};
let body = column![
header,
Space::new(0, 6),
Space::new().height(6),
proton_section,
tools_section,
autostart_section,
@@ -1731,20 +1738,25 @@ pub fn run(config: &Config) -> Result<CloseAction> {
let config = config.clone();
let close_action: Arc<Mutex<Option<CloseAction>>> = Arc::new(Mutex::new(None));
let ca = close_action.clone();
iced::application(|_: &Dashboard| String::from("umutray"), update, view)
iced::application(
move || {
let cfg = config.clone();
let load_font = iced::font::load(iced_fonts::BOOTSTRAP_FONT_BYTES).map(|_| Message::FontLoaded);
(Dashboard::new(cfg, ca.clone()), load_font)
},
update,
view,
)
.title(|_: &Dashboard| String::from("umutray"))
.subscription(subscription)
.theme(|_| Theme::Dark)
.theme(Theme::Dark)
.window(iced::window::Settings {
size: iced::Size::new(640.0, 600.0),
min_size: Some(iced::Size::new(480.0, 400.0)),
exit_on_close_request: false,
..Default::default()
})
.run_with(move || {
let cfg = config.clone();
let load_font = iced::font::load(std::borrow::Cow::Borrowed(iced_fonts::BOOTSTRAP_FONT_BYTES)).map(|_| Message::FontLoaded);
(Dashboard::new(cfg, ca.clone()), load_font)
})
.run()
.map_err(|e| anyhow::anyhow!("iced: {e}"))?;
let action = close_action.lock().unwrap().unwrap_or(CloseAction::Quit);
+41 -30
View File
@@ -498,7 +498,7 @@ fn view_picking(state: &State) -> Element<'_, Message> {
.padding(Padding::from([6, 0]))
.into()
} else {
Space::new(0, 0).into()
Space::new().into()
};
// ── Next button ───────────────────────────────────────────────────────────
@@ -620,7 +620,7 @@ fn view_install(state: &State) -> Element<'_, Message> {
.padding([6, 14])
.into()
} else {
Space::new(0, 0).into()
Space::new().into()
};
// ── Header ────────────────────────────────────────────────────────────────
@@ -632,7 +632,7 @@ fn view_install(state: &State) -> Element<'_, Message> {
}),
]
.spacing(0),
iced::widget::horizontal_space(),
Space::new().width(Length::Fill),
back_el,
]
.align_y(Alignment::Start);
@@ -651,7 +651,7 @@ fn view_install(state: &State) -> Element<'_, Message> {
.into();
section_card(inner)
} else {
Space::new(0, 0).into()
Space::new().into()
};
// ── Status card ───────────────────────────────────────────────────────────
@@ -670,7 +670,7 @@ fn view_install(state: &State) -> Element<'_, Message> {
StatusKind::Neutral,
)
} else {
Space::new(0, 0).into()
Space::new().into()
};
// ── Progress bar ──────────────────────────────────────────────────────────
@@ -690,7 +690,7 @@ fn view_install(state: &State) -> Element<'_, Message> {
.spacing(6),
)
} else {
Space::new(0, 0).into()
Space::new().into()
};
// ── Finished banner ───────────────────────────────────────────────────────
@@ -721,7 +721,7 @@ fn view_install(state: &State) -> Element<'_, Message> {
})
.into()
} else {
Space::new(0, 0).into()
Space::new().into()
};
// ── Log toggle + pane ─────────────────────────────────────────────────────
@@ -757,7 +757,7 @@ fn view_install(state: &State) -> Element<'_, Message> {
toggle_btn.into()
}
} else {
Space::new(0, 0).into()
Space::new().into()
};
// ── Action button ─────────────────────────────────────────────────────────
@@ -832,7 +832,7 @@ fn view_install(state: &State) -> Element<'_, Message> {
.align_x(Alignment::End)
.into()
}
Stage::Picking => Space::new(0, 0).into(),
Stage::Picking => Space::new().into(),
};
// ── Assembly ──────────────────────────────────────────────────────────────
@@ -859,21 +859,15 @@ pub fn run(config: &Config, launcher: &Launcher) -> Result<()> {
let config = config.clone();
let launcher = launcher.clone();
let title = format!("umutray — {}", launcher.display);
iced::application(move |_: &State| title.clone(), update, view)
.subscription(subscription)
.theme(|_| Theme::Dark)
.window(iced::window::Settings {
size: iced::Size::new(520.0, 440.0),
resizable: false,
..Default::default()
})
.run_with(move || {
let has_url = launcher.installer_url.is_some();
let url = launcher.installer_url.clone().unwrap_or_default();
iced::application(
move || {
let mut state = State::new_install(config.clone(), launcher.clone());
if has_url {
state.source = launcher.installer_url.unwrap_or_default();
state.source = url.clone();
}
let load_font = iced::font::load(std::borrow::Cow::Borrowed(iced_fonts::BOOTSTRAP_FONT_BYTES))
let load_font = iced::font::load(iced_fonts::BOOTSTRAP_FONT_BYTES)
.map(|_| Message::FontLoaded);
let init_task = if has_url {
Task::batch([load_font, Task::done(Message::AutoDownload)])
@@ -881,25 +875,42 @@ pub fn run(config: &Config, launcher: &Launcher) -> Result<()> {
load_font
};
(state, init_task)
})
.map_err(|e| anyhow::anyhow!("iced: {e}"))
}
pub fn run_new(config: &Config) -> Result<()> {
let config = config.clone();
iced::application(|_: &State| "umutray — Add Launcher".to_string(), update, view)
},
update,
view,
)
.title(move |_: &State| title.clone())
.subscription(subscription)
.theme(|_| Theme::Dark)
.theme(Theme::Dark)
.window(iced::window::Settings {
size: iced::Size::new(520.0, 440.0),
resizable: false,
..Default::default()
})
.run_with(move || {
let load_font = iced::font::load(std::borrow::Cow::Borrowed(iced_fonts::BOOTSTRAP_FONT_BYTES))
.run()
.map_err(|e| anyhow::anyhow!("iced: {e}"))
}
pub fn run_new(config: &Config) -> Result<()> {
let config = config.clone();
iced::application(
move || {
let load_font = iced::font::load(iced_fonts::BOOTSTRAP_FONT_BYTES)
.map(|_| Message::FontLoaded);
(State::new_picking(config.clone()), load_font)
},
update,
view,
)
.title(|_: &State| "umutray — Add Launcher".to_string())
.subscription(subscription)
.theme(Theme::Dark)
.window(iced::window::Settings {
size: iced::Size::new(520.0, 440.0),
resizable: false,
..Default::default()
})
.run()
.map_err(|e| anyhow::anyhow!("iced: {e}"))
}
+4 -1
View File
@@ -55,7 +55,7 @@ pub const BORDER_CLR: Color = Color {
// ── Helpers ────────────────────────────────────────────────────────────────
/// Bootstrap-icon helper — keeps call sites tidy.
pub fn icon(codepoint: &str, size: u16) -> iced::widget::Text<'static> {
pub fn icon(codepoint: &str, size: u32) -> iced::widget::Text<'static> {
text(codepoint.to_owned())
.font(iced::Font::with_name("bootstrap-icons"))
.size(size)
@@ -93,6 +93,7 @@ pub fn btn_accent(_theme: &Theme, status: button::Status) -> button::Style {
radius: 8.0.into(),
},
shadow: NO_SHADOW,
snap: false,
}
}
@@ -132,6 +133,7 @@ pub fn btn_ghost(_theme: &Theme, status: button::Status) -> button::Style {
radius: 8.0.into(),
},
shadow: NO_SHADOW,
snap: false,
}
}
@@ -157,6 +159,7 @@ pub fn btn_danger(_theme: &Theme, status: button::Status) -> button::Style {
radius: 8.0.into(),
},
shadow: NO_SHADOW,
snap: false,
}
}
+6 -5
View File
@@ -87,7 +87,7 @@ pub struct UmuTray {
pub running: HashMap<String, bool>,
/// Set after the tray spawns so Quit can shut down the SNI item
/// cleanly instead of yanking it off the bus via exit().
pub handle: Option<ksni::Handle<UmuTray>>,
pub handle: Option<ksni::blocking::Handle<UmuTray>>,
}
impl ksni::Tray for UmuTray {
@@ -312,7 +312,7 @@ pub fn run(config: &Config) -> Result<()> {
/// A handle that can shut down the tray from another thread.
pub struct TrayHandle {
inner: ksni::Handle<UmuTray>,
inner: ksni::blocking::Handle<UmuTray>,
}
impl TrayHandle {
@@ -335,9 +335,10 @@ pub fn spawn(config: &Config) -> TrayHandle {
handle: None,
};
let service = ksni::TrayService::new(tray);
let handle = service.handle();
service.spawn();
let handle = {
use ksni::blocking::TrayMethods;
tray.spawn().expect("Failed to spawn tray service")
};
// Hand the tray a clone of its own handle so Quit can shut down cleanly.
let handle_for_self = handle.clone();