diff --git a/data/app.driftwood.Driftwood.policy b/data/app.driftwood.Driftwood.policy
new file mode 100644
index 0000000..381b25c
--- /dev/null
+++ b/data/app.driftwood.Driftwood.policy
@@ -0,0 +1,20 @@
+
+
+
+
+ Driftwood
+ https://github.com/driftwood-appimage
+
+
+ Install AppImage system-wide
+ Authentication is required to install an AppImage for all users.
+
+ auth_admin
+ auth_admin
+ auth_admin_keep
+
+
+
+
diff --git a/src/core/database.rs b/src/core/database.rs
index a25590a..7d17d9c 100644
--- a/src/core/database.rs
+++ b/src/core/database.rs
@@ -1778,6 +1778,14 @@ impl Database {
Ok(())
}
+ pub fn set_system_wide(&self, id: i64, system_wide: bool) -> SqlResult<()> {
+ self.conn.execute(
+ "UPDATE appimages SET system_wide = ?2 WHERE id = ?1",
+ params![id, system_wide as i32],
+ )?;
+ Ok(())
+ }
+
// --- Launch statistics ---
pub fn get_top_launched(&self, limit: i32) -> SqlResult> {
diff --git a/src/core/integrator.rs b/src/core/integrator.rs
index b3baaae..507392d 100644
--- a/src/core/integrator.rs
+++ b/src/core/integrator.rs
@@ -535,6 +535,160 @@ pub fn set_default_app(
Ok(())
}
+/// Install an AppImage system-wide via pkexec.
+/// Copies the binary to /opt/driftwood-apps/, desktop file to
+/// /usr/share/applications/, and icon to /usr/share/icons/hicolor/.
+pub fn install_system_wide(
+ record: &AppImageRecord,
+ db: &Database,
+) -> Result<(), String> {
+ let app_name = record
+ .app_name
+ .as_deref()
+ .or(Some(&record.filename))
+ .ok_or("No app name")?;
+ let app_id = make_app_id(app_name);
+
+ let dest_dir = "/opt/driftwood-apps";
+ let dest_binary = format!("{}/{}", dest_dir, record.filename);
+ let desktop_filename = format!("driftwood-{}.desktop", app_id);
+ let system_desktop = format!("/usr/share/applications/{}", desktop_filename);
+ let icon_id = format!("driftwood-{}", app_id);
+
+ // Create destination directory
+ let status = Command::new("pkexec")
+ .args(["mkdir", "-p", dest_dir])
+ .status()
+ .map_err(|e| format!("pkexec failed: {}", e))?;
+ if !status.success() {
+ return Err("Failed to create system directory".to_string());
+ }
+
+ // Copy AppImage binary
+ let status = Command::new("pkexec")
+ .args(["cp", &record.path, &dest_binary])
+ .status()
+ .map_err(|e| format!("pkexec cp failed: {}", e))?;
+ if !status.success() {
+ return Err("Failed to copy AppImage to system directory".to_string());
+ }
+
+ // Make executable
+ let status = Command::new("pkexec")
+ .args(["chmod", "+x", &dest_binary])
+ .status()
+ .map_err(|e| format!("pkexec chmod failed: {}", e))?;
+ if !status.success() {
+ return Err("Failed to set execute permission".to_string());
+ }
+
+ db.register_modification(record.id, "system_binary", &dest_binary, None).ok();
+
+ // Generate system desktop file content
+ let categories = record.categories.as_deref().unwrap_or("");
+ let comment = record.description.as_deref().unwrap_or("");
+
+ let mut desktop_content = format!("\
+[Desktop Entry]
+Type=Application
+Name={name}
+Exec=\"{exec}\" %U
+Icon={icon}
+Categories={categories}
+Comment={comment}
+Terminal=false
+X-AppImage-Managed-By=Driftwood
+",
+ name = app_name,
+ exec = escape_exec_arg(&dest_binary),
+ icon = icon_id,
+ categories = categories,
+ comment = comment,
+ );
+
+ let mime_types = record.mime_types.as_deref().unwrap_or("");
+ let wm_class = record.startup_wm_class.as_deref().unwrap_or("");
+ if !mime_types.is_empty() {
+ desktop_content.push_str(&format!("MimeType={}\n", mime_types));
+ }
+ if !wm_class.is_empty() {
+ desktop_content.push_str(&format!("StartupWMClass={}\n", wm_class));
+ }
+
+ // Write desktop file via temp file + pkexec mv
+ let tmp = std::env::temp_dir().join(&desktop_filename);
+ fs::write(&tmp, &desktop_content)
+ .map_err(|e| format!("Failed to write temp desktop file: {}", e))?;
+
+ let status = Command::new("pkexec")
+ .args(["mv", &tmp.to_string_lossy(), &system_desktop])
+ .status()
+ .map_err(|e| format!("pkexec mv failed: {}", e))?;
+ if !status.success() {
+ return Err("Failed to install system desktop file".to_string());
+ }
+
+ db.register_modification(record.id, "system_desktop", &system_desktop, None).ok();
+
+ // Copy icon if available
+ if let Some(ref cached_icon) = record.icon_path {
+ let cached = Path::new(cached_icon);
+ if cached.exists() {
+ let ext = cached.extension().and_then(|e| e.to_str()).unwrap_or("png");
+ let (subdir, icon_filename) = if ext == "svg" {
+ ("scalable/apps", format!("{}.svg", icon_id))
+ } else {
+ ("256x256/apps", format!("{}.png", icon_id))
+ };
+ let system_icon_dir = format!("/usr/share/icons/hicolor/{}", subdir);
+ let system_icon = format!("{}/{}", system_icon_dir, icon_filename);
+
+ Command::new("pkexec")
+ .args(["mkdir", "-p", &system_icon_dir])
+ .status()
+ .ok();
+
+ let status = Command::new("pkexec")
+ .args(["cp", &cached.to_string_lossy(), &system_icon])
+ .status();
+ if let Ok(s) = status {
+ if s.success() {
+ db.register_modification(record.id, "system_icon", &system_icon, None).ok();
+ }
+ }
+ }
+ }
+
+ db.set_system_wide(record.id, true).ok();
+
+ Ok(())
+}
+
+/// Remove a system-wide installation.
+pub fn remove_system_wide(
+ db: &Database,
+ appimage_id: i64,
+) -> Result<(), String> {
+ let mods = db.get_modifications(appimage_id).unwrap_or_default();
+ for m in &mods {
+ match m.mod_type.as_str() {
+ "system_binary" | "system_desktop" | "system_icon" => {
+ let status = Command::new("pkexec")
+ .args(["rm", "-f", &m.file_path])
+ .status();
+ if let Ok(s) = status {
+ if s.success() {
+ db.remove_modification(m.id).ok();
+ }
+ }
+ }
+ _ => {}
+ }
+ }
+ db.set_system_wide(appimage_id, false).ok();
+ Ok(())
+}
+
fn update_desktop_database() {
let apps_dir = applications_dir();
Command::new("update-desktop-database")
diff --git a/src/ui/detail_view.rs b/src/ui/detail_view.rs
index d1457a6..6602aeb 100644
--- a/src/ui/detail_view.rs
+++ b/src/ui/detail_view.rs
@@ -1017,6 +1017,55 @@ fn build_system_tab(record: &AppImageRecord, db: &Rc, toast_overlay: &
});
integration_group.add(&wm_class_row);
+ // System-wide install toggle
+ if record.integrated {
+ let syswide_row = adw::SwitchRow::builder()
+ .title("Install system-wide")
+ .subtitle(if record.system_wide {
+ "Installed for all users in /opt/driftwood-apps/"
+ } else {
+ "Make available to all users on this computer"
+ })
+ .active(record.system_wide)
+ .tooltip_text(
+ "Copies the AppImage and its shortcut to system directories \
+ so all users on this computer can access it. Requires \
+ administrator privileges."
+ )
+ .build();
+
+ let record_sw = record.clone();
+ let db_sw = db.clone();
+ let toast_sw = toast_overlay.clone();
+ let record_id_sw = record.id;
+ syswide_row.connect_active_notify(move |row| {
+ if row.is_active() {
+ match integrator::install_system_wide(&record_sw, &db_sw) {
+ Ok(()) => {
+ toast_sw.add_toast(adw::Toast::new("Installed system-wide"));
+ }
+ Err(e) => {
+ log::error!("System-wide install failed: {}", e);
+ toast_sw.add_toast(adw::Toast::new("System-wide install failed"));
+ row.set_active(false);
+ }
+ }
+ } else {
+ match integrator::remove_system_wide(&db_sw, record_id_sw) {
+ Ok(()) => {
+ toast_sw.add_toast(adw::Toast::new("System-wide install removed"));
+ }
+ Err(e) => {
+ log::error!("Failed to remove system-wide install: {}", e);
+ toast_sw.add_toast(adw::Toast::new("Failed to remove system-wide install"));
+ row.set_active(true);
+ }
+ }
+ }
+ });
+ integration_group.add(&syswide_row);
+ }
+
inner.append(&integration_group);
// Version Rollback group