feat(settings): add Rebuild & Install self-update button

Settings panel now shows the current version and a "Rebuild & Install
Latest" button that:
  1. git pull from the embedded source directory (CARGO_MANIFEST_DIR)
  2. makepkg -sf in packaging/
  3. pkexec pacman -U (graphical polkit auth prompt)

Reports the installed version on success; surfaces the failing step on
error. Update runs off the UI thread so the window stays responsive.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-04-18 23:41:07 -07:00
parent 3c78e1586f
commit f70498158a
+96
View File
@@ -61,6 +61,9 @@ pub enum Message {
ServiceInstall,
ServiceUninstall,
ServiceActionDone(Result<(), String>),
// Self-update
UpdateApp,
UpdateAppDone(Result<String, String>),
}
struct Dashboard {
@@ -87,6 +90,8 @@ struct Dashboard {
proton_versions: Vec<String>,
service_busy: bool,
service_status: String,
update_busy: bool,
update_status: String,
}
impl Dashboard {
@@ -118,6 +123,8 @@ impl Dashboard {
proton_versions,
service_busy: false,
service_status: String::new(),
update_busy: false,
update_status: String::new(),
}
}
}
@@ -570,6 +577,22 @@ fn update(state: &mut Dashboard, msg: Message) -> Task<Message> {
}
Task::none()
}
Message::UpdateApp => {
state.update_busy = true;
state.update_status = "Pulling latest changes…".into();
Task::perform(
async_blocking(run_self_update),
Message::UpdateAppDone,
)
}
Message::UpdateAppDone(res) => {
state.update_busy = false;
state.update_status = match res {
Ok(ver) => format!("✓ Updated to {ver} — restart the app to apply."),
Err(e) => format!("Update failed: {e}"),
};
Task::none()
}
}
}
@@ -1155,6 +1178,18 @@ fn view_settings(state: &Dashboard) -> Element<'_, Message> {
text(svc_status_label).size(13),
row![svc_install_btn, svc_uninstall_btn,].spacing(10),
text(&state.service_status).size(12),
iced::widget::horizontal_rule(1),
row![
text(format!("Version: {}", env!("CARGO_PKG_VERSION"))).size(13),
iced::widget::horizontal_space(),
]
.align_y(Alignment::Center),
button(
text(if state.update_busy { "Updating…" } else { "Rebuild & Install Latest" }).size(13),
)
.on_press_maybe((!state.update_busy).then_some(Message::UpdateApp))
.style(button::secondary),
text(&state.update_status).size(12),
]
.spacing(10)
.padding(20);
@@ -1165,6 +1200,67 @@ fn view_settings(state: &Dashboard) -> Element<'_, Message> {
.into()
}
fn run_self_update() -> Result<String, String> {
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
if !src.exists() {
return Err(format!("Source directory not found: {}", src.display()));
}
let pkg_dir = src.join("packaging");
// 1. git pull
let out = std::process::Command::new("git")
.args(["-C", src.to_str().unwrap_or("."), "pull"])
.output()
.map_err(|e| format!("git pull: {e}"))?;
if !out.status.success() {
return Err(format!("git pull failed: {}", String::from_utf8_lossy(&out.stderr)));
}
// 2. makepkg -sf --noconfirm
let out = std::process::Command::new("makepkg")
.args(["-sf", "--noconfirm"])
.current_dir(&pkg_dir)
.output()
.map_err(|e| format!("makepkg: {e}"))?;
if !out.status.success() {
return Err(format!("makepkg failed: {}", String::from_utf8_lossy(&out.stderr)));
}
// 3. Find the freshly built package
let pkg = std::fs::read_dir(&pkg_dir)
.map_err(|e| e.to_string())?
.flatten()
.filter_map(|e| {
let name = e.file_name();
let n = name.to_string_lossy().to_string();
if n.ends_with(".pkg.tar.zst") && !n.contains("debug") {
Some(e.path())
} else {
None
}
})
.max_by_key(|p| p.metadata().and_then(|m| m.modified()).ok())
.ok_or_else(|| "No .pkg.tar.zst found after makepkg".to_string())?;
// 4. Install with pkexec (graphical auth prompt)
let out = std::process::Command::new("pkexec")
.args(["pacman", "-U", "--noconfirm", pkg.to_str().unwrap_or("")])
.output()
.map_err(|e| format!("pkexec: {e}"))?;
if !out.status.success() {
return Err(format!("Install failed: {}", String::from_utf8_lossy(&out.stderr)));
}
// Extract version from package filename e.g. umutray-0.1.0-4-x86_64.pkg.tar.zst
let ver = pkg
.file_name()
.and_then(|n| n.to_str())
.and_then(|n| n.split('-').nth(1))
.unwrap_or("?")
.to_string();
Ok(ver)
}
pub fn run(config: &Config) -> Result<()> {
let config = config.clone();
iced::application(|_: &Dashboard| String::from("umutray"), update, view)