Compare commits
95
Commits
main
..
6beca34f70
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6beca34f70 | ||
|
|
09ef0f48e0 | ||
|
|
58d8b44866 | ||
|
|
99f60b8529 | ||
|
|
da9568df61 | ||
|
|
7e55d5796f | ||
|
|
abb69dc753 | ||
|
|
79519c500a | ||
|
|
d11546efc6 | ||
|
|
4b939f044a | ||
|
|
f89aafca6a | ||
|
|
92c51dc39e | ||
|
|
86b047572a | ||
|
|
7697983045 | ||
|
|
5254de4a52 | ||
|
|
91882abc3c | ||
|
|
6a546396ed | ||
|
|
68918af00a | ||
|
|
4596cce11d | ||
|
|
7e93f1632e | ||
|
|
e311f6b230 | ||
|
|
80a81191ab | ||
|
|
d0291caf75 | ||
|
|
6adc47a8d3 | ||
|
|
df186e74db | ||
|
|
46f46db98c | ||
|
|
b23f9e14f8 | ||
|
|
3eb15af2c6 | ||
|
|
cf8f072820 | ||
|
|
582344ad48 | ||
|
|
31da61f3da | ||
|
|
f12e74ba2b | ||
|
|
2704ebb316 | ||
|
|
2108b0f3d8 | ||
|
|
f2abfba753 | ||
|
|
27eb9f259d | ||
|
|
c622057830 | ||
|
|
585320b363 | ||
|
|
8cf71ae858 | ||
|
|
97c7250666 | ||
|
|
01d453d329 | ||
|
|
45b45c0724 | ||
|
|
00a1ed3599 | ||
|
|
843af0a8a5 | ||
|
|
1a6eb4ec99 | ||
|
|
c311fb27c3 | ||
|
|
c50b61fc83 | ||
|
|
0777ae1a26 | ||
|
|
9cf4f1126c | ||
|
|
730452072f | ||
|
|
5b668bd931 | ||
|
|
8934e66f6d | ||
|
|
78f004ff4f | ||
|
|
f5685e03e3 | ||
|
|
e7a056c75a | ||
|
|
4173f33bbf | ||
|
|
8dd0dc71ed | ||
|
|
6e2e7e8e36 | ||
|
|
5ef3789c7d | ||
|
|
830c3cad9d | ||
|
|
e9343da249 | ||
|
|
f87403794e | ||
|
|
c9f032292a | ||
|
|
df3efa3b51 | ||
|
|
bb71f5d4d5 | ||
|
|
d493516efa | ||
|
|
c9c9c0341b | ||
|
|
65a1ea78fe | ||
|
|
8362e066f7 | ||
|
|
1bb7a3bdc0 | ||
|
|
39b773fed5 | ||
|
|
fc3ee9ba8f | ||
|
|
6526f92a6f | ||
|
|
423323d5a9 | ||
|
|
a7ed3742fb | ||
|
|
33cc8a757a | ||
|
|
4f7d8560f1 | ||
|
|
2f56439b6e | ||
|
|
4b8592801b | ||
|
|
9c60dfb252 | ||
|
|
fae87a753a | ||
|
|
2ea85ac700 | ||
|
|
1a608c0109 | ||
|
|
1bafb135f0 | ||
|
|
3642fbd542 | ||
|
|
0aeb445554 | ||
|
|
1ac5f41d69 | ||
|
|
66fc582cce | ||
|
|
7852e7ab4b | ||
|
|
19791168f3 | ||
|
|
df533dda0a | ||
|
|
dc97a6cbbc | ||
|
|
fa28955919 | ||
|
|
588b1b1525 | ||
|
|
cd61b480ad |
+137
@@ -0,0 +1,137 @@
|
||||
# Contributing to Driftwood
|
||||
|
||||
## Building from source
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Rust 1.75+ and Cargo
|
||||
- GTK 4.16+ development headers (`libgtk-4-dev` or `gtk4-devel`)
|
||||
- libadwaita 1.6+ development headers (`libadwaita-1-dev` or `libadwaita-devel`)
|
||||
- SQLite 3 development headers
|
||||
- gettext development headers
|
||||
- `glib-compile-resources` and `glib-compile-schemas` (from `libglib2.0-dev-bin`)
|
||||
|
||||
### Quick start
|
||||
|
||||
```sh
|
||||
git clone https://github.com/driftwood-app/driftwood
|
||||
cd driftwood
|
||||
cargo build
|
||||
cargo run
|
||||
```
|
||||
|
||||
### Running tests
|
||||
|
||||
```sh
|
||||
cargo test
|
||||
```
|
||||
|
||||
## Project structure
|
||||
|
||||
```
|
||||
driftwood/
|
||||
Cargo.toml # Rust package manifest
|
||||
build.rs # GResource and GSettings compilation
|
||||
meson.build # Meson build system for installation
|
||||
|
||||
src/
|
||||
main.rs # Entry point, GResource init, CLI dispatch
|
||||
application.rs # GtkApplication subclass, CSS loading, app actions
|
||||
window.rs # Main window, navigation, scanning orchestration
|
||||
config.rs # App ID and version constants
|
||||
cli.rs # Command-line interface (clap)
|
||||
i18n.rs # Internationalization (gettext wrappers)
|
||||
|
||||
core/ # Backend logic (no GTK dependencies)
|
||||
database.rs # SQLite database (rusqlite), all queries
|
||||
discovery.rs # Filesystem scanning, AppImage detection, SHA256
|
||||
inspector.rs # AppImage metadata extraction (icon, desktop entry)
|
||||
integrator.rs # Desktop integration (.desktop files, icons)
|
||||
launcher.rs # AppImage launching with FUSE/sandbox support
|
||||
updater.rs # Update checking and applying (GitHub, zsync)
|
||||
fuse.rs # FUSE status detection
|
||||
wayland.rs # Wayland compatibility analysis
|
||||
security.rs # CVE scanning via OSV.dev API
|
||||
duplicates.rs # Duplicate and multi-version detection
|
||||
footprint.rs # Disk footprint analysis (config/data/cache)
|
||||
orphan.rs # Orphaned desktop entry detection and cleanup
|
||||
|
||||
ui/ # GTK4/libadwaita UI components
|
||||
library_view.rs # Main grid/list view of AppImages
|
||||
app_card.rs # Individual AppImage card widget
|
||||
detail_view.rs # Full detail page for a single AppImage
|
||||
dashboard.rs # System health dashboard
|
||||
preferences.rs # Preferences dialog
|
||||
update_dialog.rs # Update check and apply dialog
|
||||
duplicate_dialog.rs # Duplicate resolution dialog
|
||||
cleanup_wizard.rs # Disk space reclamation wizard
|
||||
security_report.rs # Security scan results view
|
||||
integration_dialog.rs # Desktop integration confirmation
|
||||
widgets.rs # Shared utility widgets (badges, sections)
|
||||
|
||||
data/
|
||||
app.driftwood.Driftwood.gschema.xml # GSettings schema
|
||||
app.driftwood.Driftwood.desktop # Desktop entry for Driftwood itself
|
||||
app.driftwood.Driftwood.metainfo.xml # AppStream metadata
|
||||
resources.gresource.xml # GResource manifest
|
||||
resources/style.css # Application CSS
|
||||
|
||||
po/ # Translation files
|
||||
POTFILES.in # Files with translatable strings
|
||||
LINGUAS # Available translations
|
||||
|
||||
build-aux/ # Build helpers
|
||||
app.driftwood.Driftwood.json # Flatpak manifest
|
||||
build-appimage.sh # AppImage build script
|
||||
|
||||
packaging/
|
||||
PKGBUILD # Arch Linux AUR package
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The codebase is split into three layers:
|
||||
|
||||
1. **core/** - Pure Rust business logic. No GTK dependencies. Can be tested
|
||||
independently. Each module handles one concern.
|
||||
|
||||
2. **ui/** - GTK4/libadwaita widgets. Each view is a function that builds a
|
||||
widget tree. Uses `Rc<Database>` for shared database access.
|
||||
|
||||
3. **window.rs / application.rs** - Orchestration layer. Connects UI to core,
|
||||
handles navigation, spawns background threads for scanning.
|
||||
|
||||
Background work (scanning, update checks, security scans) runs on
|
||||
`gio::spawn_blocking` threads. Results are sent back to the main thread
|
||||
via `glib::spawn_future_local`.
|
||||
|
||||
## Coding conventions
|
||||
|
||||
- Follow standard Rust formatting (`cargo fmt`)
|
||||
- All new code must compile with zero warnings
|
||||
- Add tests for core/ modules (81+ tests currently)
|
||||
- Use `log::info!`, `log::warn!`, `log::error!` for diagnostics
|
||||
- User-facing strings should be wrapped in `i18n()` for translation
|
||||
- Use `adw::` widgets over raw `gtk::` when an Adwaita equivalent exists
|
||||
- Status badges use CSS classes: `badge-success`, `badge-warning`, `badge-error`
|
||||
|
||||
## Database
|
||||
|
||||
SQLite database stored at `~/.local/share/driftwood/driftwood.db`. Schema
|
||||
migrates automatically (v1 through v4). All queries are in `core/database.rs`.
|
||||
|
||||
## Testing
|
||||
|
||||
```sh
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Run tests for a specific module
|
||||
cargo test core::database
|
||||
cargo test core::updater
|
||||
|
||||
# Run with output
|
||||
cargo test -- --nocapture
|
||||
```
|
||||
|
||||
Tests use `Database::open_in_memory()` for isolation.
|
||||
@@ -5,7 +5,7 @@
|
||||
<policyconfig>
|
||||
|
||||
<vendor>Driftwood</vendor>
|
||||
<vendor_url>https://git.lashman.live/lashman/driftwood</vendor_url>
|
||||
<vendor_url>https://github.com/driftwood-appimage</vendor_url>
|
||||
|
||||
<action id="app.driftwood.Driftwood.system-install">
|
||||
<description>Install AppImage system-wide</description>
|
||||
|
||||
+2
-2
@@ -5,8 +5,8 @@ pkgver=0.1.0
|
||||
pkgrel=1
|
||||
pkgdesc='Modern AppImage manager for GNOME desktops'
|
||||
arch=('x86_64')
|
||||
url='https://git.lashman.live/lashman/driftwood'
|
||||
license=('CC0-1.0')
|
||||
url='https://github.com/driftwood-app/driftwood'
|
||||
license=('GPL-3.0-or-later')
|
||||
depends=(
|
||||
'gtk4'
|
||||
'libadwaita'
|
||||
|
||||
+24
-19
@@ -29,6 +29,7 @@ pub struct AppImageRecord {
|
||||
pub first_seen: String,
|
||||
pub last_scanned: String,
|
||||
pub file_modified: Option<String>,
|
||||
// Phase 2 fields
|
||||
pub fuse_status: Option<String>,
|
||||
pub wayland_status: Option<String>,
|
||||
pub update_info: Option<String>,
|
||||
@@ -37,17 +38,20 @@ pub struct AppImageRecord {
|
||||
pub update_checked: Option<String>,
|
||||
pub update_url: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
// Phase 3 fields
|
||||
pub sandbox_mode: Option<String>,
|
||||
// Phase 5 fields
|
||||
pub runtime_wayland_status: Option<String>,
|
||||
pub runtime_wayland_checked: Option<String>,
|
||||
// Async analysis pipeline
|
||||
pub analysis_status: Option<String>,
|
||||
// Custom launch arguments
|
||||
pub launch_args: Option<String>,
|
||||
// Phase 6 fields
|
||||
pub tags: Option<String>,
|
||||
pub pinned: bool,
|
||||
pub avg_startup_ms: Option<i64>,
|
||||
// extended metadata
|
||||
// Phase 9 fields - comprehensive metadata
|
||||
pub appstream_id: Option<String>,
|
||||
pub appstream_description: Option<String>,
|
||||
pub generic_name: Option<String>,
|
||||
@@ -65,7 +69,7 @@ pub struct AppImageRecord {
|
||||
pub desktop_actions: Option<String>,
|
||||
pub has_signature: bool,
|
||||
pub screenshot_urls: Option<String>,
|
||||
// system modification tracking
|
||||
// Phase 11 fields - system modification tracking
|
||||
pub previous_version_path: Option<String>,
|
||||
pub source_url: Option<String>,
|
||||
pub autostart: bool,
|
||||
@@ -342,7 +346,7 @@ impl Database {
|
||||
}
|
||||
|
||||
fn init_schema(&self) -> SqlResult<()> {
|
||||
// base tables
|
||||
// Phase 1 base tables
|
||||
self.conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS schema_version (
|
||||
version INTEGER NOT NULL
|
||||
@@ -531,8 +535,8 @@ impl Database {
|
||||
}
|
||||
|
||||
fn migrate_to_v2(&self) -> SqlResult<()> {
|
||||
// Add columns to appimages table
|
||||
let extra_columns = [
|
||||
// Add Phase 2 columns to appimages table
|
||||
let phase2_columns = [
|
||||
"fuse_status TEXT",
|
||||
"wayland_status TEXT",
|
||||
"update_info TEXT",
|
||||
@@ -542,11 +546,12 @@ impl Database {
|
||||
"update_url TEXT",
|
||||
"notes TEXT",
|
||||
];
|
||||
for col in &extra_columns {
|
||||
for col in &phase2_columns {
|
||||
let sql = format!("ALTER TABLE appimages ADD COLUMN {}", col);
|
||||
self.conn.execute_batch(&sql).ok();
|
||||
}
|
||||
|
||||
// Phase 2 tables
|
||||
self.conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS launch_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -590,7 +595,7 @@ impl Database {
|
||||
}
|
||||
|
||||
fn migrate_to_v3(&self) -> SqlResult<()> {
|
||||
// security scanning
|
||||
// Phase 3 tables: security scanning
|
||||
self.conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS bundled_libraries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -1707,7 +1712,7 @@ impl Database {
|
||||
self.conn.query_row("SELECT COUNT(*) FROM appimages", [], |row| row.get(0))
|
||||
}
|
||||
|
||||
// --- Status updates ---
|
||||
// --- Phase 2: Status updates ---
|
||||
|
||||
pub fn update_fuse_status(&self, id: i64, status: &str) -> SqlResult<()> {
|
||||
self.conn.execute(
|
||||
@@ -1806,7 +1811,7 @@ impl Database {
|
||||
rows.collect()
|
||||
}
|
||||
|
||||
// --- Launch tracking ---
|
||||
// --- Phase 2: Launch tracking ---
|
||||
|
||||
pub fn record_launch(&self, appimage_id: i64, source: &str) -> SqlResult<()> {
|
||||
self.conn.execute(
|
||||
@@ -1849,7 +1854,7 @@ impl Database {
|
||||
rows.collect()
|
||||
}
|
||||
|
||||
// --- Update history ---
|
||||
// --- Phase 2: Update history ---
|
||||
|
||||
pub fn record_update(
|
||||
&self,
|
||||
@@ -1869,7 +1874,7 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// --- Security scanning ---
|
||||
// --- Phase 3: Security scanning ---
|
||||
|
||||
pub fn clear_bundled_libraries(&self, appimage_id: i64) -> SqlResult<()> {
|
||||
self.conn.execute(
|
||||
@@ -2021,7 +2026,7 @@ impl Database {
|
||||
Ok(summary)
|
||||
}
|
||||
|
||||
// --- App data paths ---
|
||||
// --- Phase 3: App data paths ---
|
||||
|
||||
pub fn insert_app_data_path(
|
||||
&self,
|
||||
@@ -2101,7 +2106,7 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// --- Runtime Wayland ---
|
||||
// --- Phase 5: Runtime Wayland ---
|
||||
|
||||
pub fn update_runtime_wayland_status(&self, id: i64, status: &str) -> SqlResult<()> {
|
||||
self.conn.execute(
|
||||
@@ -2111,7 +2116,7 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// --- Config Backups ---
|
||||
// --- Phase 5: Config Backups ---
|
||||
|
||||
pub fn insert_config_backup(
|
||||
&self,
|
||||
@@ -2181,7 +2186,7 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// --- CVE Notifications ---
|
||||
// --- Phase 5: CVE Notifications ---
|
||||
|
||||
pub fn has_cve_been_notified(&self, appimage_id: i64, cve_id: &str) -> SqlResult<bool> {
|
||||
let count: i32 = self.conn.query_row(
|
||||
@@ -2206,7 +2211,7 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// --- Sandbox Profiles ---
|
||||
// --- Phase 5: Sandbox Profiles ---
|
||||
|
||||
pub fn insert_sandbox_profile(
|
||||
&self,
|
||||
@@ -2276,7 +2281,7 @@ impl Database {
|
||||
rows.collect()
|
||||
}
|
||||
|
||||
// --- Tags, Pin, Startup Time ---
|
||||
// --- Phase 6: Tags, Pin, Startup Time ---
|
||||
|
||||
pub fn update_tags(&self, id: i64, tags: Option<&str>) -> SqlResult<()> {
|
||||
self.conn.execute(
|
||||
@@ -2350,7 +2355,7 @@ impl Database {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Runtime Updates ---
|
||||
// --- Phase 5: Runtime Updates ---
|
||||
|
||||
pub fn record_runtime_update(
|
||||
&self,
|
||||
@@ -3254,7 +3259,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_status_updates() {
|
||||
fn test_phase2_status_updates() {
|
||||
let db = Database::open_in_memory().unwrap();
|
||||
let id = db.upsert_appimage("/path/app.AppImage", "app.AppImage", Some(2), 1000, true, None).unwrap();
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ pub fn detect_duplicates(db: &Database) -> Vec<DuplicateGroup> {
|
||||
|
||||
let mut groups = Vec::new();
|
||||
|
||||
// Find exact duplicates by SHA256 hash
|
||||
// Phase 1: Find exact duplicates by SHA256 hash
|
||||
let hash_groups = group_by_hash(&records);
|
||||
for (hash, members) in &hash_groups {
|
||||
if members.len() > 1 {
|
||||
@@ -95,7 +95,7 @@ pub fn detect_duplicates(db: &Database) -> Vec<DuplicateGroup> {
|
||||
}
|
||||
}
|
||||
|
||||
// Find same app name groups (excluding already-found exact dupes)
|
||||
// Phase 2: Find same app name groups (excluding already-found exact dupes)
|
||||
let exact_dupe_ids: std::collections::HashSet<i64> = groups
|
||||
.iter()
|
||||
.flat_map(|g| g.members.iter().map(|m| m.record.id))
|
||||
|
||||
@@ -117,7 +117,7 @@ pub fn show_drop_dialog(
|
||||
let on_complete_ref = on_complete.clone();
|
||||
|
||||
glib::spawn_future_local(async move {
|
||||
// Fast registration (copy + DB upsert only)
|
||||
// Phase 1: Fast registration (copy + DB upsert only)
|
||||
let result = gio::spawn_blocking(move || {
|
||||
register_dropped_files(&files, copy)
|
||||
})
|
||||
@@ -143,7 +143,7 @@ pub fn show_drop_dialog(
|
||||
toast_ref.add_toast(widgets::info_toast(&msg));
|
||||
}
|
||||
|
||||
// Background analysis for each file
|
||||
// Phase 2: Background analysis for each file
|
||||
let on_complete_bg = on_complete_ref.clone();
|
||||
for reg in registered {
|
||||
let on_complete_inner = on_complete_bg.clone();
|
||||
|
||||
+7
-7
@@ -1571,11 +1571,11 @@ impl DriftwoodWindow {
|
||||
let toast_overlay = self.imp().toast_overlay.get().unwrap().clone();
|
||||
let window_weak = self.downgrade();
|
||||
|
||||
// Two-pass scan:
|
||||
// Fast pass: discover files, upsert into DB, mark pending analysis
|
||||
// Background pass: heavy analysis per file
|
||||
// Two-phase scan:
|
||||
// Phase 1 (fast): discover files, upsert into DB, mark pending analysis
|
||||
// Phase 2 (background): heavy analysis per file
|
||||
glib::spawn_future_local(async move {
|
||||
// Fast registration
|
||||
// Phase 1: Fast registration
|
||||
let result = gio::spawn_blocking(move || {
|
||||
let bg_db = Database::open().expect("Failed to open database for scan");
|
||||
let start = Instant::now();
|
||||
@@ -1646,7 +1646,7 @@ impl DriftwoodWindow {
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Scan: {} files, {} new, {} removed, {} skipped (unchanged), took {}ms",
|
||||
"Scan phase 1: {} files, {} new, {} removed, {} skipped (unchanged), took {}ms",
|
||||
total, new_count, removed_count, skipped_count, start.elapsed().as_millis()
|
||||
);
|
||||
|
||||
@@ -1665,7 +1665,7 @@ impl DriftwoodWindow {
|
||||
.await;
|
||||
|
||||
if let Ok((total, new_count, needs_analysis)) = result {
|
||||
// Dismiss the "Scanning..." toast now that fast scan is done
|
||||
// Dismiss the "Scanning..." toast now that Phase 1 is done
|
||||
if let Some(ref toast) = scan_toast {
|
||||
toast.dismiss();
|
||||
}
|
||||
@@ -1689,7 +1689,7 @@ impl DriftwoodWindow {
|
||||
};
|
||||
toast_overlay.add_toast(widgets::info_toast(&msg));
|
||||
|
||||
// Background analysis per file with debounced UI refresh
|
||||
// Phase 2: Background analysis per file with debounced UI refresh
|
||||
let running = analysis::running_count();
|
||||
if running > 0 {
|
||||
log::info!("Analyzing {} AppImage(s) in background ({} already running)", needs_analysis.len(), running);
|
||||
|
||||
Reference in New Issue
Block a user