Implement Driftwood AppImage manager - Phases 1 and 2

Phase 1 - Application scaffolding:
- GTK4/libadwaita application window with AdwNavigationView
- GSettings-backed window state persistence
- GResource-compiled CSS and schema
- Library view with grid/list toggle, search, sorting, filtering
- Detail view with file info, desktop integration controls
- Preferences window with scan directories, theme, behavior settings
- CLI with list, scan, integrate, remove, clean, inspect commands
- AppImage discovery, metadata extraction, desktop integration
- Orphaned desktop entry detection and cleanup
- AppImage packaging script

Phase 2 - Intelligence layer:
- Database schema v2 with migration for status tracking columns
- FUSE detection engine (libfuse2/3, fusermount, /dev/fuse, AppImageLauncher)
- Wayland awareness engine (session type, toolkit detection, XWayland)
- Update info parsing from AppImage ELF sections (.upd_info)
- GitHub/GitLab Releases API integration for update checking
- Update download with progress tracking and atomic apply
- Launch wrapper with FUSE auto-detection and usage tracking
- Duplicate and multi-version detection with recommendations
- Dashboard with system health, library stats, disk usage
- Update check dialog (single and batch)
- Duplicate resolution dialog
- Status badges on library cards and detail view
- Extended CLI: status, check-updates, duplicates, launch commands

49 tests passing across all modules.
This commit is contained in:
lashman
2026-02-26 23:04:27 +02:00
parent 588b1b1525
commit fa28955919
33 changed files with 10401 additions and 0 deletions
+145
View File
@@ -0,0 +1,145 @@
use adw::prelude::*;
use adw::subclass::prelude::*;
use gtk::gio;
use std::cell::OnceCell;
use crate::config::{APP_ID, VERSION};
use crate::window::DriftwoodWindow;
mod imp {
use super::*;
pub struct DriftwoodApplication {
pub settings: OnceCell<gio::Settings>,
}
impl Default for DriftwoodApplication {
fn default() -> Self {
Self {
settings: OnceCell::new(),
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for DriftwoodApplication {
const NAME: &'static str = "DriftwoodApplication";
type Type = super::DriftwoodApplication;
type ParentType = adw::Application;
}
impl ObjectImpl for DriftwoodApplication {}
impl ApplicationImpl for DriftwoodApplication {
fn startup(&self) {
self.parent_startup();
let app = self.obj();
app.setup_css();
app.setup_theme();
app.setup_actions();
}
fn activate(&self) {
self.parent_activate();
let app = self.obj();
// Present existing window or create a new one
if let Some(window) = app.active_window() {
window.present();
} else {
let window = DriftwoodWindow::new(&*app);
window.present();
}
}
}
impl GtkApplicationImpl for DriftwoodApplication {}
impl AdwApplicationImpl for DriftwoodApplication {}
}
glib::wrapper! {
pub struct DriftwoodApplication(ObjectSubclass<imp::DriftwoodApplication>)
@extends adw::Application, gtk::Application, gio::Application,
@implements gio::ActionGroup, gio::ActionMap;
}
impl DriftwoodApplication {
pub fn new(app_id: &str, flags: &gio::ApplicationFlags) -> Self {
glib::Object::builder()
.property("application-id", app_id)
.property("flags", flags)
.build()
}
fn setup_css(&self) {
let provider = gtk::CssProvider::new();
provider.load_from_resource("/app/driftwood/Driftwood/style.css");
gtk::style_context_add_provider_for_display(
&gtk::gdk::Display::default().expect("Could not get default display"),
&provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}
fn setup_theme(&self) {
let settings = gio::Settings::new(APP_ID);
Self::apply_color_scheme(&settings);
settings.connect_changed(Some("color-scheme"), |settings, _| {
Self::apply_color_scheme(settings);
});
// Store settings on the imp struct so it stays alive
// (otherwise the connect_changed signal gets dropped)
self.imp()
.settings
.set(settings)
.expect("Theme settings already initialized");
}
fn apply_color_scheme(settings: &gio::Settings) {
let value = settings.string("color-scheme");
let scheme = match value.as_str() {
"force-light" => adw::ColorScheme::ForceLight,
"force-dark" => adw::ColorScheme::ForceDark,
_ => adw::ColorScheme::Default,
};
adw::StyleManager::default().set_color_scheme(scheme);
}
fn setup_actions(&self) {
// Quit action (Ctrl+Q)
let quit_action = gio::ActionEntry::builder("quit")
.activate(|app: &Self, _, _| {
if let Some(window) = app.active_window() {
window.close();
}
app.quit();
})
.build();
// About action
let about_action = gio::ActionEntry::builder("about")
.activate(|app: &Self, _, _| {
app.show_about_dialog();
})
.build();
self.add_action_entries([quit_action, about_action]);
self.set_accels_for_action("app.quit", &["<Control>q"]);
}
fn show_about_dialog(&self) {
let dialog = adw::AboutDialog::builder()
.application_name("Driftwood")
.application_icon(APP_ID)
.version(VERSION)
.developer_name("Driftwood Contributors")
.license_type(gtk::License::Gpl30)
.comments("A modern AppImage manager for GNOME desktops")
.website("https://github.com/driftwood-app/driftwood")
.build();
dialog.present(self.active_window().as_ref());
}
}