Compare commits

...

3 Commits

Author SHA1 Message Date
lashman
6beca34f70 Add app icons, screenshots, and complete AppStream metainfo
- Add GNOME HIG-compliant app icon (scalable SVG) and symbolic variant
- Add 12 screenshots covering all major views and features
- Flesh out metainfo with screenshots, categories, URLs, content
  rating, system requirements, provides, translation, donation
  and contact links
- Update AppImage build script to bundle GTK plugin, symbolic
  icon, and metainfo
- Update meson.build with icon installation rules
- Remove About dialog from application menu
- Remove unused user guide and audit tool
2026-03-01 14:46:41 +02:00
lashman
09ef0f48e0 Rewrite README with better header and project framing 2026-03-01 13:12:45 +02:00
lashman
58d8b44866 Rewrite README with comprehensive feature documentation 2026-03-01 12:54:55 +02:00
22 changed files with 568 additions and 1337 deletions

481
README.md
View File

@@ -1,100 +1,437 @@
# Driftwood
<div align="center">
A modern GTK4/libadwaita AppImage manager for GNOME desktops.
# :evergreen_tree: Driftwood
Driftwood discovers, inspects, integrates, updates, and audits AppImage files
with a clean GNOME-native interface built for the Wayland era.
**Your apps. Your computer. No middleman.**
## Features
A friendly, modern AppImage manager for Linux desktops -
built by the community, owned by no one.
- **Library management** - Scan directories to discover AppImages, view them in
grid or list mode with status badges for FUSE, Wayland, and update status
- **Desktop integration** - Create .desktop files and install icons with one click
- **FUSE and Wayland detection** - Automatically detect compatibility and suggest
launch methods (direct, extract-and-run, or sandboxed)
- **Update checking** - Read embedded update information (GitHub Releases, GitLab,
zsync) and check for newer versions
- **Security scanning** - Extract bundled shared libraries and check them against
the OSV.dev vulnerability database
- **Duplicate detection** - Find AppImages that are different versions of the same
app or identical files in different locations
- **Disk footprint analysis** - Discover config, data, and cache files associated
with each AppImage
- **Sandboxing** - Optional Firejail sandbox support per-app
- **Orphan cleanup** - Detect and remove .desktop files for AppImages that no
longer exist
- **CLI interface** - Full command-line access to all core features
<br />
## Requirements
![License: CC0-1.0](https://img.shields.io/badge/license-CC0--1.0-bb4444?style=flat-square)
![Built with Rust](https://img.shields.io/badge/built%20with-Rust-d4652a?style=flat-square)
![GTK4 + libadwaita](https://img.shields.io/badge/GTK4-libadwaita-4a86cf?style=flat-square)
![WCAG 2.2 AAA](https://img.shields.io/badge/WCAG%202.2-AAA-228833?style=flat-square)
![No Telemetry](https://img.shields.io/badge/telemetry-none-778899?style=flat-square)
- GTK 4.16+
- libadwaita 1.6+
- SQLite 3
- gettext
<br />
Optional:
- firejail (for sandboxed launches)
- fuse2/fuse3 (for AppImage FUSE mounting)
- appimageupdate (for delta updates)
*No accounts. No tracking. No data harvested. No profit motive.*
*Just a tool that helps you manage your apps, and gets out of the way.*
## Building from source
</div>
---
## :sparkles: What is Driftwood?
**Driftwood is a graphical app manager for AppImages.** AppImages are portable
Linux applications that come as a single file - no installation needed, no
package manager required, no root access, no corporate app store standing between
you and the software you want to run.
But managing AppImages by hand can be confusing. Making them show up in your app
menu, checking for updates, knowing if they're safe - that's a lot of work,
especially if you're new to Linux.
Driftwood handles all of that for you with a clean, easy-to-use interface. It
runs entirely on your machine - nothing is sent anywhere, nothing is collected,
nothing is monetized.
---
## :raising_hand: Who is this for?
- **People new to Linux** who want a simple, welcoming way to manage their apps
- **Anyone who downloads AppImages** and wants them organized, updated, and
integrated into their desktop without friction
- **People who care about privacy** and want to know exactly what their apps
contain and whether any bundled libraries have known security issues
- **People who believe software should be accessible to everyone** - Driftwood
meets WCAG 2.2 AAA accessibility standards
- **Power users** who want a full command-line interface alongside the GUI
---
## :framed_picture: What does it look like?
Driftwood uses GTK4 and libadwaita, so it looks and feels like a native GNOME
app. It has three main views:
| View | What it does |
|------|-------------|
| **Installed** | Shows all your AppImages in a grid or list with status badges |
| **Catalog** | Browse and discover new AppImage apps from the community |
| **Updates** | See which apps have newer versions and update them |
Click any app to see its full details - version info, security status, disk
usage, screenshots, and more.
---
## :star2: Features
### :package: App Library
- **Automatic scanning** - Driftwood finds all AppImage files in your chosen
folders (like `~/Applications` or `~/Downloads`)
- **Grid and list views** - See your apps as cards with icons, or as a detailed
list - whichever you prefer
- **Search and sort** - Find apps by name, sort by name, size, or when you added
them
- **Tags** - Label your apps with custom tags and filter by them
- **Selection mode** - Select multiple apps at once for batch actions
- **Drag and drop** - Drop an AppImage file onto the window to add it
### :globe_with_meridians: App Catalog
- **Browse community apps** - Discover AppImage apps shared by developers and
communities - no walled garden, no curation fees, no gatekeeping
- **Category filters** - Browse by Audio, Games, Graphics, Development, and more
- **Featured apps** - A rotating carousel of highlighted apps
- **Search** - Find apps by name across the entire catalog
- **App details** - See descriptions, screenshots, star counts, and download
numbers
- **One-click install** - Download and add apps directly from the catalog
### :arrow_up: Updates
- **Check for updates** - Scan all your apps for newer versions with one click
- **Update all at once** - Or update apps individually, your choice
- **Release notes** - See what changed in each new version before you decide
- **Automatic backups** - Optionally save old versions before updating, so you
can roll back if something breaks
- **Multiple update sources** - Works with GitHub Releases, GitLab Releases,
zsync delta updates, and OCS feeds
### :rocket: Desktop Integration
- **Add to app menu** - Make any AppImage show up in your desktop's application
menu with one click
- **Icon installation** - Automatically extracts and installs the app's icon
- **Autostart** - Set apps to launch automatically when you log in
- **Custom launch options** - Add command-line arguments or environment variables
per app
- **Fully reversible** - Remove any app from your menu just as easily as you
added it. Every change Driftwood makes to your system is tracked and can be
undone
### :shield: Security
Your apps shouldn't be a black box. Driftwood helps you see what's inside.
- **Vulnerability scanning** - Extracts the shared libraries bundled inside each
AppImage and checks them against the OSV.dev vulnerability database
- **CVE details** - Shows the severity (Critical, High, Medium, Low), CVSS
scores, and which library versions are affected
- **Security report** - Generate a full report of all your apps and export it as
HTML, JSON, or CSV
- **Desktop notifications** - Get notified when a new vulnerability is found in
one of your apps
- **Signature verification** - Check GPG signatures and SHA256 checksums
- **Configurable thresholds** - Choose whether to be notified about only critical
issues or everything
### :jigsaw: FUSE and Wayland Compatibility
- **FUSE detection** - Checks whether your system can mount AppImages and tells
you exactly what to install if it can't
- **Guided FUSE setup** - A step-by-step wizard that shows you the right install
command for your distribution - no searching forums, no guesswork
- **Wayland compatibility** - Detects whether you're running Wayland or X11 and
flags any compatibility concerns
- **Fallback launching** - If FUSE isn't available, Driftwood can extract and run
AppImages directly - you're never locked out of your own software
### :broom: Cleanup and Disk Management
- **Duplicate finder** - Detects when you have multiple copies of the same app
or different versions taking up space, and shows how much you'd save
- **Disk footprint** - Shows config, cache, and data folders each app has created
on your system - no hidden clutter
- **Cleanup wizard** - A guided walkthrough that finds orphaned desktop entries,
leftover caches, and duplicates, then lets you choose what to remove
- **Orphan cleanup** - Finds and removes menu entries for AppImages that no
longer exist on disk
### :bar_chart: Dashboard
- **System overview** - A quick health check showing FUSE status, Wayland status,
total apps, storage used, and apps needing updates
- **At-a-glance stats** - Everything about your AppImage setup in one place
### :gear: Preferences
- **Appearance** - Follow your system's light/dark theme, or pick one
- **Scan directories** - Choose which folders Driftwood watches for AppImages
- **Update settings** - Configure automatic update checking, backup behavior,
and how long to keep old versions
- **Security settings** - Enable automatic scanning, notifications, and set a
GitHub token for higher API rate limits
- **Catalog settings** - Toggle automatic metadata enrichment and removable media
watching
### :lock: Sandboxing
- **Firejail support** - Launch apps inside a Firejail sandbox for extra
isolation
- **Per-app profiles** - Each app can have its own sandbox configuration
- **Profile management** - Use local, community, or default Firejail profiles
### :wheelchair: Accessibility
Software that only works for some people doesn't really work. Driftwood is built
to meet WCAG 2.2 AAA accessibility standards:
- **Full keyboard navigation** - Every single feature is reachable without a
mouse
- **Screen reader support** - All buttons, images, and status changes are
announced to screen readers like Orca
- **Focus management** - Clear, visible focus indicators (3px outlines) on all
interactive elements
- **44px minimum target sizes** - All buttons meet the AAA touch/click target
standard
- **High contrast support** - Enhanced borders and colors when your system uses
high contrast mode
- **No timing requirements** - Nothing in the app requires fast reactions or
races against a clock
- **Semantic roles** - Headings, alerts, status regions, and live announcements
are all properly marked up for assistive technology
- **Automated testing** - Ships with an AT-SPI audit tool that walks the live
accessibility tree and checks for violations
### :keyboard: Keyboard Shortcuts
| Shortcut | Action |
|----------|--------|
| `Ctrl+1` | Switch to Installed view |
| `Ctrl+2` | Switch to Catalog view |
| `Ctrl+3` | Switch to Updates view |
| `Ctrl+F` | Search |
| `Ctrl+R` or `F5` | Scan for AppImages |
| `Ctrl+U` | Check for updates |
| `Ctrl+K` | Open command palette |
| `Ctrl+D` | Open dashboard |
| `Ctrl+,` | Open preferences |
| `Ctrl+?` | Show keyboard shortcuts |
| `Ctrl+Q` | Quit |
### :desktop_computer: Command Line Interface
Every feature is also available from the terminal. The GUI and CLI share the same
database, so they always stay in sync.
```sh
# Development build (uses cargo directly)
driftwood scan # Find AppImages in your configured folders
driftwood list # Show all known AppImages
driftwood list --format json # Output as JSON
driftwood inspect ~/Apps/MyApp.AppImage # Show app metadata
driftwood integrate ~/Apps/MyApp.AppImage # Add to desktop menu
driftwood remove ~/Apps/MyApp.AppImage # Remove from desktop menu
driftwood launch ~/Apps/MyApp.AppImage # Run the app
driftwood launch --sandbox ~/Apps/MyApp.AppImage # Run in sandbox
driftwood check-updates # Check all apps for updates
driftwood update-all # Update everything
driftwood verify ~/Apps/MyApp.AppImage # Check integrity
driftwood verify ~/Apps/MyApp.AppImage --sha256 abc123... # Verify hash
driftwood security # Scan all apps for vulnerabilities
driftwood duplicates # Find duplicate AppImages
driftwood footprint ~/Apps/MyApp.AppImage # Show disk usage
driftwood status # System compatibility check
driftwood clean-orphans # Remove stale menu entries
driftwood export # Export app list to JSON
driftwood export --output ~/backup.json
driftwood import ~/backup.json # Import app list
driftwood autostart ~/Apps/MyApp.AppImage --enable # Autostart on login
driftwood autostart ~/Apps/MyApp.AppImage --disable
driftwood purge # Remove ALL Driftwood system modifications
```
---
## :penguin: Which Linux distributions does it work on?
Driftwood works on any Linux distribution that has GTK 4.16+ and libadwaita 1.6+.
In practice, that means most modern distributions released from 2024 onward.
| Distribution | Status | Notes |
|-------------|--------|-------|
| **Ubuntu 24.04+** | :white_check_mark: Works | GTK4 and libadwaita in default repos |
| **Fedora 40+** | :white_check_mark: Works | Excellent GTK4 support out of the box |
| **Arch Linux** | :white_check_mark: Works | Rolling release, always has the latest |
| **Manjaro** | :white_check_mark: Works | Same packages as Arch |
| **openSUSE Tumbleweed** | :white_check_mark: Works | Rolling release with current GTK4 |
| **Debian Testing/Sid** | :white_check_mark: Works | Stable may have older GTK versions |
| **Linux Mint 22+** | :white_check_mark: Works | Based on Ubuntu 24.04 |
| **Pop!_OS 24.04+** | :white_check_mark: Works | Based on Ubuntu |
| **elementary OS 8+** | :white_check_mark: Works | GTK4-based Pantheon desktop |
| **Gentoo** | :white_check_mark: Works | Build from source |
| **Alpine** | :white_check_mark: Works | GTK4 in community repos |
| **NixOS** | :white_check_mark: Works | Package GTK4 + libadwaita via Nix |
**Desktop environment**: Driftwood is designed for GNOME but works on any desktop
that supports GTK4 applications (KDE Plasma, Cinnamon, XFCE, Budgie, Sway, etc.).
It looks best on GNOME and GNOME-based desktops because it uses libadwaita for
styling.
---
## :wrench: System Requirements
### What you need
| Requirement | Version | What it is |
|------------|---------|-----------|
| **GTK** | 4.16 or newer | The toolkit that draws the interface |
| **libadwaita** | 1.6 or newer | GNOME's design library for modern-looking apps |
| **SQLite** | 3 | A small, local database engine (stores your app library) |
| **gettext** | any | Handles translations for different languages |
### Optional (but recommended)
| Package | What it does |
|---------|-------------|
| **libfuse2** or **libfuse3** | Lets AppImages mount themselves (most AppImages need this to run) |
| **firejail** | Runs apps in a security sandbox for extra isolation |
| **appimageupdate** | Enables fast delta updates (downloads only what changed, not the whole file) |
### For building from source
| Package | What it is |
|---------|-----------|
| **Rust 1.75+** and **Cargo** | The programming language and build tool |
| **Meson** | Build system (only for system-wide installation) |
| **libgtk-4-dev** | GTK4 development headers |
| **libadwaita-1-dev** | libadwaita development headers |
| **libsqlite3-dev** | SQLite development headers |
| **libglib2.0-dev-bin** | Provides `glib-compile-resources` and `glib-compile-schemas` |
> Package names vary between distributions. On Fedora, use `-devel` instead
> of `-dev` (for example, `gtk4-devel` instead of `libgtk-4-dev`). On Arch,
> the packages are `gtk4`, `libadwaita`, and `sqlite`.
---
## :building_construction: Building from Source
### Quick development build
```sh
git clone https://git.lashman.live/lashman/driftwood.git
cd driftwood
cargo build
cargo run
```
# System installation (uses meson)
### System-wide installation
```sh
meson setup build --prefix=/usr
meson compile -C build
sudo meson install -C build
```
## CLI usage
After installing, Driftwood will appear in your application menu.
---
## :package: Packaging
Packaging files are included so anyone can build packages for their distribution:
| Format | Location | Notes |
|--------|----------|-------|
| **Flatpak** | `build-aux/app.driftwood.Driftwood.json` | Flatpak manifest |
| **Arch Linux (AUR)** | `packaging/PKGBUILD` | For makepkg |
| **AppImage** | `packaging/build-appimage.sh` | Build script |
If you package Driftwood for a distribution not listed here, let us know - we'd
love to include it.
---
## :open_file_folder: Where Driftwood Stores Things
Driftwood keeps everything local. Nothing leaves your machine.
| What | Location |
|------|----------|
| App database | `~/.local/share/driftwood/driftwood.db` |
| Cached icons | `~/.local/share/driftwood/icons/` |
| Desktop entries | `~/.local/share/applications/driftwood-*.desktop` |
| Installed icons | `~/.local/share/icons/hicolor/` |
| Autostart entries | `~/.config/autostart/` |
| Settings | GSettings (`app.driftwood.Driftwood`) |
| Backups | `~/.local/share/driftwood/backups/` |
Every modification Driftwood makes to your system is recorded in its database.
You can undo all of it at any time with `driftwood purge` or through the UI.
Your system, your rules.
---
## :test_tube: Accessibility Testing
Driftwood ships with an automated accessibility audit tool that uses AT-SPI (the
Linux accessibility bus) to walk the live widget tree and check for WCAG 2.2
violations:
```sh
# Scan configured directories for AppImages
driftwood scan
# Full AAA-level audit (builds and launches automatically)
python3 tools/a11y-audit.py --level aaa
# List all known AppImages
driftwood list
driftwood list --format json
# Attach to an already-running instance
python3 tools/a11y-audit.py --no-launch --level aaa
# Inspect a specific AppImage
driftwood inspect ~/Applications/Firefox.AppImage
# AA level only
python3 tools/a11y-audit.py --level aa
# Integrate into desktop menu
driftwood integrate ~/Applications/Firefox.AppImage
# Check for updates
driftwood check-updates
# Run a security scan
driftwood security
driftwood security ~/Applications/Firefox.AppImage
# Launch with tracking
driftwood launch ~/Applications/Firefox.AppImage
driftwood launch --sandbox ~/Applications/Firefox.AppImage
# Find duplicates
driftwood duplicates
# Show disk footprint
driftwood footprint ~/Applications/Firefox.AppImage
# System status
driftwood status
# Clean orphaned entries
driftwood clean-orphans
# Verbose output with live region inventory
python3 tools/a11y-audit.py --level aaa --verbose
```
## Packaging
The tool checks for:
- **Flatpak**: See `build-aux/app.driftwood.Driftwood.json`
- **Arch Linux (AUR)**: See `packaging/PKGBUILD`
- Interactive widgets without accessible names (SC 4.1.2)
- Images without alt text or decorative marking (SC 1.1.1)
- Headings without level attributes (SC 1.3.1)
- Widgets that aren't keyboard-focusable (SC 2.1.1)
- Target sizes below 24px (AA) and 44px (AAA)
- Windows without titles (SC 2.4.8)
- Vague link text (SC 2.4.9)
- Content regions without headings (SC 2.4.10)
- Keyboard traps (SC 2.1.3)
## License
Results are printed to the terminal and saved as `a11y-report.json` for CI
integration.
CC0-1.0 (Public Domain)
---
## :raised_hands: Contributing
Driftwood is a community project. There's no company behind it, no investors,
no roadmap driven by profit - just people building something useful together.
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on how to contribute code,
translations, bug reports, or ideas. Every contribution matters, and everyone
who helps is a co-owner of this project.
---
## :page_facing_up: License
**CC0-1.0 - Public Domain**
Driftwood belongs to everyone. It is released into the public domain under the
[CC0 1.0 Universal](LICENSE) license. You can copy, modify, distribute, and use
it for any purpose - personal, educational, commercial, anything - without
asking permission, without paying anyone, without restrictions.
Knowledge and tools should be shared freely. This is ours, and it's yours.

View File

@@ -23,24 +23,105 @@
<li>Duplicate detection and disk space analysis</li>
<li>Firejail sandboxing support</li>
<li>Orphaned configuration cleanup</li>
<li>Browse and install from the AppImageHub catalog</li>
</ul>
</description>
<launchable type="desktop-id">app.driftwood.Driftwood.desktop</launchable>
<icon type="stock">app.driftwood.Driftwood</icon>
<url type="homepage">https://github.com/driftwood-app/driftwood</url>
<url type="bugtracker">https://github.com/driftwood-app/driftwood/issues</url>
<launchable type="desktop-id">app.driftwood.Driftwood.desktop</launchable>
<developer id="app.driftwood">
<name>Driftwood Contributors</name>
</developer>
<url type="homepage">https://git.lashman.live/lashman/driftwood</url>
<url type="bugtracker">https://git.lashman.live/lashman/driftwood/issues</url>
<url type="vcs-browser">https://git.lashman.live/lashman/driftwood</url>
<url type="donation">https://ko-fi.com/lashman</url>
<url type="contact">mailto:lashman@robotbrush.com</url>
<url type="contribute">https://git.lashman.live/lashman/driftwood</url>
<update_contact>lashman@robotbrush.com</update_contact>
<branding>
<color type="primary" scheme_preference="light">#8ff0a4</color>
<color type="primary" scheme_preference="dark">#26a269</color>
</branding>
<content_rating type="oars-1.1" />
<screenshots>
<screenshot type="default">
<caption>Library grid view showing installed AppImages</caption>
<image type="source" width="1151" height="864">https://git.lashman.live/lashman/driftwood/raw/branch/main/data/screenshots/02-library-grid.png</image>
</screenshot>
<screenshot>
<caption>Library list view with app details</caption>
<image type="source" width="1151" height="864">https://git.lashman.live/lashman/driftwood/raw/branch/main/data/screenshots/03-library-list.png</image>
</screenshot>
<screenshot>
<caption>App detail view with description and compatibility badges</caption>
<image type="source" width="1151" height="864">https://git.lashman.live/lashman/driftwood/raw/branch/main/data/screenshots/04-app-detail-about.png</image>
</screenshot>
<screenshot>
<caption>App screenshots, links, and update information</caption>
<image type="source" width="1151" height="864">https://git.lashman.live/lashman/driftwood/raw/branch/main/data/screenshots/05-app-detail-screenshots.png</image>
</screenshot>
<screenshot>
<caption>Desktop integration and file type management</caption>
<image type="source" width="1151" height="864">https://git.lashman.live/lashman/driftwood/raw/branch/main/data/screenshots/06-app-detail-integration.png</image>
</screenshot>
<screenshot>
<caption>System dashboard with FUSE, Wayland, and library status</caption>
<image type="source" width="1151" height="864">https://git.lashman.live/lashman/driftwood/raw/branch/main/data/screenshots/07-dashboard.png</image>
</screenshot>
<screenshot>
<caption>Preferences with scan locations and automation settings</caption>
<image type="source" width="1151" height="864">https://git.lashman.live/lashman/driftwood/raw/branch/main/data/screenshots/08-preferences.png</image>
</screenshot>
<screenshot>
<caption>Drag-and-drop to add AppImage files</caption>
<image type="source" width="1151" height="864">https://git.lashman.live/lashman/driftwood/raw/branch/main/data/screenshots/09-drag-and-drop.png</image>
</screenshot>
<screenshot>
<caption>App catalog with featured apps and category browsing</caption>
<image type="source" width="1151" height="864">https://git.lashman.live/lashman/driftwood/raw/branch/main/data/screenshots/10-catalog-browse.png</image>
</screenshot>
<screenshot>
<caption>Catalog app detail with install button and statistics</caption>
<image type="source" width="1151" height="864">https://git.lashman.live/lashman/driftwood/raw/branch/main/data/screenshots/11-catalog-app-detail.png</image>
</screenshot>
<screenshot>
<caption>Catalog category view with sorting and filtering</caption>
<image type="source" width="1151" height="864">https://git.lashman.live/lashman/driftwood/raw/branch/main/data/screenshots/12-catalog-category.png</image>
</screenshot>
<screenshot>
<caption>Catalog refresh with download progress</caption>
<image type="source" width="1151" height="864">https://git.lashman.live/lashman/driftwood/raw/branch/main/data/screenshots/01-catalog-loading.png</image>
</screenshot>
</screenshots>
<categories>
<category>System</category>
<category>PackageManager</category>
<category>GTK</category>
</categories>
<keywords>
<keyword>AppImage</keyword>
<keyword>Application</keyword>
<keyword>Manager</keyword>
<keyword>Package</keyword>
<keyword>FUSE</keyword>
<keyword>Wayland</keyword>
<keyword>Security</keyword>
<keyword>Sandbox</keyword>
<keyword>Catalog</keyword>
<keyword>Integration</keyword>
</keywords>
<content_rating type="oars-1.1">
<content_attribute id="social-info">mild</content_attribute>
</content_rating>
<requires>
<display_length compare="ge">360</display_length>
@@ -51,18 +132,18 @@
<control>pointing</control>
</recommends>
<keywords>
<keyword>AppImage</keyword>
<keyword>Application</keyword>
<keyword>Manager</keyword>
<keyword>Package</keyword>
<keyword>FUSE</keyword>
<keyword>Wayland</keyword>
<keyword>Security</keyword>
</keywords>
<supports>
<internet>first-run</internet>
</supports>
<provides>
<binary>driftwood</binary>
</provides>
<translation type="gettext">app.driftwood.Driftwood</translation>
<releases>
<release version="0.1.0" date="2026-02-26">
<release version="0.1.0" date="2026-02-26" type="stable">
<description>
<p>Initial release of Driftwood with core features:</p>
<ul>
@@ -75,6 +156,7 @@
<li>Firejail sandbox support</li>
<li>Orphan cleanup and disk reclamation wizard</li>
<li>CLI interface with scan, list, launch, and inspect commands</li>
<li>AppImageHub catalog browsing and one-click install</li>
</ul>
</description>
</release>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
<!-- Chin/depth layer (GNOME Green 5) -->
<rect x="10" y="14" width="108" height="104" rx="24" fill="#26a269" />
<!-- Main face (GNOME Green 4) -->
<rect x="10" y="10" width="108" height="104" rx="24" fill="#2ec27e" />
<!-- Driftwood symbol -->
<g transform="translate(-8, -10) scale(1.44)">
<path fill="none" stroke="white" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M52.104,56.311c0-3.775,3.061-6.836,6.836-6.836c3.777,0,6.836,3.061,6.836,6.836 M62.293,55.785c0-1.652-1.34-2.99-2.994-2.99c-1.65,0-2.988,1.338-2.988,2.99 M48.948,56.311h19.983 M59.789,46.319l-13.996-1.577c-3.155-1.053-4.207-7.363-4.207-7.363l-3.154,2.104c0,0,2.103,4.207-1.053,4.207l-5.784-0.525c-3.194,0-5.784,2.59-5.784,5.785l23.137,7.361c0-5.519,4.474-9.991,9.991-9.991c5.52,0,9.992,4.473,9.992,9.991 M45.793,50l-6.311-3.154l-9.465-1.053 M35.276,48.949L39.482,50 M37.379,59.466l10.518,3.155h10.517 M74.189,56.311H80.5l-8.414-3.155 M64.725,59.466H53.156 M31.068,57.362l-6.31-2.104 M19.5,50l18.932,6.311" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="15 33 70 34">
<path fill="none" stroke="#241f31" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M52.104,56.311c0-3.775,3.061-6.836,6.836-6.836c3.777,0,6.836,3.061,6.836,6.836 M62.293,55.785c0-1.652-1.34-2.99-2.994-2.99c-1.65,0-2.988,1.338-2.988,2.99 M48.948,56.311h19.983 M59.789,46.319l-13.996-1.577c-3.155-1.053-4.207-7.363-4.207-7.363l-3.154,2.104c0,0,2.103,4.207-1.053,4.207l-5.784-0.525c-3.194,0-5.784,2.59-5.784,5.785l23.137,7.361c0-5.519,4.474-9.991,9.991-9.991c5.52,0,9.992,4.473,9.992,9.991 M45.793,50l-6.311-3.154l-9.465-1.053 M35.276,48.949L39.482,50 M37.379,59.466l10.518,3.155h10.517 M74.189,56.311H80.5l-8.414-3.155 M64.725,59.466H53.156 M31.068,57.362l-6.31-2.104 M19.5,50l18.932,6.311" />
</svg>

After

Width:  |  Height:  |  Size: 868 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

View File

@@ -1,246 +0,0 @@
# Driftwood User Guide
## Getting started
### Installation
**From source:**
```sh
cargo build --release
sudo install -Dm755 target/release/driftwood /usr/local/bin/driftwood
```
**Arch Linux (AUR):**
```sh
yay -S driftwood
```
**Flatpak:**
```sh
flatpak install app.driftwood.Driftwood
```
### First launch
When you first open Driftwood, you'll see an empty state with two options:
- **Scan Now** - Immediately scan the default directories (`~/Applications` and
`~/Downloads`) for AppImage files
- **Preferences** - Configure which directories to scan and other settings
Driftwood will discover all AppImage files (both Type 1 and Type 2) in your
configured directories and add them to its library.
## Library view
The main screen shows all your discovered AppImages in either grid or list mode.
Toggle between views with the button in the header bar.
### Status badges
Each AppImage card shows colored badges indicating:
- **Wayland status** - Green (native), yellow (XWayland), red (X11 only)
- **FUSE status** - Green (native FUSE), yellow (extract-and-run), red (cannot launch)
- **Update available** - Blue badge when a newer version is detected
- **Security** - Red badge if known vulnerabilities are found
### Searching
Use the search bar to filter AppImages by name or file path. The search is
debounced - it waits 150ms after you stop typing before filtering.
### Keyboard shortcuts
- **Ctrl+Q** - Quit
- **Ctrl+D** - Open dashboard
- **Ctrl+U** - Check for updates
## Detail view
Click any AppImage card to see its full detail page. The detail view has
these sections:
### Identity
App name, version, developer, description, and categories extracted from
the AppImage's embedded .desktop file.
### Desktop integration
Shows whether the AppImage is integrated into your desktop menu. You can
integrate or remove integration from here.
### Runtime compatibility
FUSE status (how the AppImage can be mounted) and Wayland compatibility
(whether the app supports Wayland natively or needs XWayland).
### Sandboxing
Toggle Firejail sandboxing for this AppImage. When enabled, the app
launches inside a Firejail container with `--appimage` flag. Requires
firejail to be installed.
### Updates
Shows the update type (GitHub Releases, GitLab, zsync), current and latest
versions, and lets you check for and apply updates.
### Usage
Launch count and last launched date.
### Security
Results of CVE scanning against bundled libraries. Shows counts by severity
(critical, high, medium, low).
### Disk footprint
Config, data, and cache directories associated with this AppImage. Shows
estimated size and discovery confidence.
### File details
File path, size, SHA256 hash, AppImage type, architecture, and timestamps.
## Scanning
### Automatic scanning
By default, Driftwood scans on startup. Disable this in Preferences under
Behavior > "Scan on startup".
### Manual scanning
Use the "Scan for AppImages" option in the hamburger menu or run:
```sh
driftwood scan
```
### Scan optimization
On subsequent scans, Driftwood skips files that haven't changed (same size
and modification time) and already have all analysis complete. This makes
re-scans much faster.
### Adding scan directories
Go to Preferences > General > Scan Locations to add or remove directories.
Subdirectories are not scanned recursively.
## Desktop integration
Driftwood creates standard .desktop files in `~/.local/share/applications/`
with the prefix `driftwood-`. Icons are installed to
`~/.local/share/icons/hicolor/`.
To integrate an AppImage:
1. Open its detail view
2. Click "Integrate" in the Desktop Integration section
3. Confirm in the integration dialog
To remove integration:
1. Open its detail view
2. Click "Remove Integration"
## Updates
### Checking for updates
- Single app: Open detail view and click "Check for Updates"
- All apps: Use the hamburger menu "Check for Updates" or `driftwood check-updates`
### Applying updates
When an update is available, click "Update Now" in the update dialog. Driftwood
downloads the new version and replaces the old file.
### Old version cleanup
After a successful update, Driftwood handles the old version based on your
preference (Preferences > Behavior > "After updating an AppImage"):
- **Ask each time** (default) - Shows a dialog asking whether to remove the backup
- **Remove old version** - Automatically deletes the backup
- **Keep backup** - Saves the old version with a `.old` extension
## Security scanning
Driftwood extracts the list of shared libraries (.so files) bundled inside each
AppImage and queries the OSV.dev vulnerability database for known CVEs.
### Running a scan
- Single app: Open detail view and click "Run Security Scan"
- All apps: `driftwood security`
- Single app CLI: `driftwood security ~/path/to/app.AppImage`
### Interpreting results
Results show CVE IDs grouped by severity. Each CVE includes:
- CVE identifier and severity level
- CVSS score (if available)
- Summary of the vulnerability
- Affected library and version
- Fixed version (if known)
### Limitations
- Not all bundled libraries can be identified
- Version detection uses heuristics and may be inaccurate
- Results should be treated as advisory, not definitive
## Duplicate detection
Driftwood detects:
- **Same app, different versions** - Multiple version files of the same application
- **Identical files** - Same SHA256 hash in different locations
Access via the hamburger menu "Find Duplicates" or `driftwood duplicates`.
## Disk cleanup
The cleanup wizard (hamburger menu > "Disk Cleanup") helps reclaim space by:
- Identifying orphaned desktop entries for deleted AppImages
- Finding associated config/data/cache directories
- Showing total reclaimable space
## Orphaned entries
When an AppImage is deleted outside of Driftwood, its .desktop file and icon
remain. Driftwood detects these orphans and offers to clean them up.
- Automatic detection on the dashboard
- Manual cleanup via hamburger menu or `driftwood clean-orphans`
## Dashboard
The dashboard (Ctrl+D or hamburger menu) shows system health:
- FUSE availability (fuse2, fuse3, or none)
- Wayland session information
- Total disk usage by AppImages
- Orphaned entry count
- Security alert summary
## CLI reference
```
driftwood # Launch the GUI
driftwood scan # Scan for AppImages
driftwood list # List all AppImages (table format)
driftwood list --format json # List as JSON
driftwood inspect <path> # Show AppImage metadata
driftwood integrate <path> # Create .desktop file and icon
driftwood remove <path> # Remove desktop integration
driftwood launch <path> # Launch with tracking
driftwood launch --sandbox <path> # Launch in Firejail
driftwood check-updates # Check all for updates
driftwood duplicates # Find duplicates
driftwood security # Scan all for CVEs
driftwood security <path> # Scan one for CVEs
driftwood footprint <path> # Show disk footprint
driftwood status # Show system status
driftwood clean-orphans # Remove orphaned entries
```
## Preferences
Access via hamburger menu > Preferences.
### General
- **Color Scheme** - Follow system, light, or dark
- **Default View** - Grid or list
- **Scan Locations** - Directories to scan
### Behavior
- **Scan on startup** - Auto-scan when the app opens
- **Check for updates** - Periodically check for newer versions
- **Auto-integrate new AppImages** - Automatically create .desktop files
- **Confirm before delete** - Show confirmation dialogs
- **After updating** - Old version cleanup policy (ask/always/never)
### Security
- **Auto-scan new AppImages** - Run CVE scan on newly discovered AppImages

View File

@@ -2,7 +2,7 @@ project(
'driftwood',
'rust',
version: '0.1.0',
license: 'GPL-3.0-or-later',
license: 'CC0-1.0',
meson_version: '>= 0.62.0',
)
@@ -28,12 +28,25 @@ install_data(
install_dir: datadir / 'metainfo',
)
# Install app icons
install_data(
'data' / 'icons' / 'hicolor' / 'scalable' / 'apps' / app_id + '.svg',
install_dir: iconsdir / 'hicolor' / 'scalable' / 'apps',
)
install_data(
'data' / 'icons' / 'hicolor' / 'symbolic' / 'apps' / app_id + '-symbolic.svg',
install_dir: iconsdir / 'hicolor' / 'symbolic' / 'apps',
)
# Compile and install GSettings schema
install_data(
'data' / app_id + '.gschema.xml',
install_dir: datadir / 'glib-2.0' / 'schemas',
)
gnome.post_install(glib_compile_schemas: true)
gnome.post_install(
glib_compile_schemas: true,
gtk_update_icon_cache: true,
)
# Build the Rust binary via Cargo
cargo = find_program('cargo')

View File

@@ -29,6 +29,8 @@ mkdir -p "$APPDIR/usr/bin"
mkdir -p "$APPDIR/usr/share/applications"
mkdir -p "$APPDIR/usr/share/glib-2.0/schemas"
mkdir -p "$APPDIR/usr/share/icons/hicolor/scalable/apps"
mkdir -p "$APPDIR/usr/share/icons/hicolor/symbolic/apps"
mkdir -p "$APPDIR/usr/share/metainfo"
# Binary
cp "target/release/driftwood" "$APPDIR/usr/bin/driftwood"
@@ -40,8 +42,8 @@ cp "data/$APP_ID.desktop" "$APPDIR/usr/share/applications/$APP_ID.desktop"
cp "data/$APP_ID.gschema.xml" "$APPDIR/usr/share/glib-2.0/schemas/$APP_ID.gschema.xml"
glib-compile-schemas "$APPDIR/usr/share/glib-2.0/schemas/"
# Icon - use a placeholder SVG if no real icon exists yet
ICON_FILE="data/icons/$APP_ID.svg"
# Icon
ICON_FILE="data/icons/hicolor/scalable/apps/$APP_ID.svg"
if [ -f "$ICON_FILE" ]; then
cp "$ICON_FILE" "$APPDIR/usr/share/icons/hicolor/scalable/apps/$APP_ID.svg"
else
@@ -56,6 +58,18 @@ else
SVGEOF
fi
# Symbolic icon
SYMBOLIC_FILE="data/icons/hicolor/symbolic/apps/$APP_ID-symbolic.svg"
if [ -f "$SYMBOLIC_FILE" ]; then
cp "$SYMBOLIC_FILE" "$APPDIR/usr/share/icons/hicolor/symbolic/apps/$APP_ID-symbolic.svg"
fi
# AppStream metainfo
METAINFO_FILE="data/$APP_ID.metainfo.xml"
if [ -f "$METAINFO_FILE" ]; then
cp "$METAINFO_FILE" "$APPDIR/usr/share/metainfo/$APP_ID.metainfo.xml"
fi
# Check for linuxdeploy
LINUXDEPLOY="${LINUXDEPLOY:-linuxdeploy}"
if ! command -v "$LINUXDEPLOY" &>/dev/null; then
@@ -77,20 +91,20 @@ if ! command -v "$LINUXDEPLOY" &>/dev/null; then
fi
# Check for GTK plugin
GTK_PLUGIN="${LINUXDEPLOY_PLUGIN_GTK:-}"
if [ -z "$GTK_PLUGIN" ]; then
if [ -x "./linuxdeploy-plugin-gtk.sh" ]; then
GTK_PLUGIN_ARG=""
if [ -x "./linuxdeploy-plugin-gtk.sh" ]; then
export DEPLOY_GTK_VERSION=4
else
GTK_PLUGIN_ARG="--plugin gtk"
echo "GTK4 plugin found - libraries will be bundled."
else
echo ""
echo "Warning: linuxdeploy-plugin-gtk not found."
echo "GTK4 libraries will not be bundled."
echo "The AppImage may only work on systems with GTK4 and libadwaita installed."
echo ""
echo "Download the plugin from:"
echo " https://github.com/nickvdp/linuxdeploy-plugin-gtk"
echo " https://github.com/linuxdeploy/linuxdeploy-plugin-gtk"
echo ""
fi
fi
echo "=== Building AppImage ==="
@@ -102,6 +116,7 @@ export GSETTINGS_SCHEMA_DIR="$APPDIR/usr/share/glib-2.0/schemas"
--appdir "$APPDIR" \
--desktop-file "$APPDIR/usr/share/applications/$APP_ID.desktop" \
--icon-file "$APPDIR/usr/share/icons/hicolor/scalable/apps/$APP_ID.svg" \
$GTK_PLUGIN_ARG \
--output appimage
echo ""

View File

@@ -3,7 +3,7 @@ use adw::subclass::prelude::*;
use gtk::gio;
use std::cell::OnceCell;
use crate::config::{APP_ID, VERSION};
use crate::config::APP_ID;
use crate::window::DriftwoodWindow;
mod imp {
@@ -118,28 +118,8 @@ impl DriftwoodApplication {
})
.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.add_action_entries([quit_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());
}
}

View File

@@ -184,7 +184,6 @@ impl DriftwoodWindow {
let section3 = gio::Menu::new();
section3.append(Some(&i18n("Preferences")), Some("win.preferences"));
section3.append(Some(&i18n("Keyboard Shortcuts")), Some("win.show-shortcuts"));
section3.append(Some(&i18n("About Driftwood")), Some("app.about"));
menu.append_section(None, &section3);
// Library view (contains header bar, search, grid/list, empty state)

View File

@@ -1,964 +0,0 @@
#!/usr/bin/env python3
"""
Automated WCAG 2.2 AAA accessibility audit for Driftwood via AT-SPI.
Launches the app, waits for it to register on the accessibility bus,
then walks the entire widget tree checking for violations at A, AA, and AAA
levels:
Level A:
- SC 1.1.1: Images with no name and no decorative marking
- SC 1.3.1: Headings missing a level property
- SC 2.1.1: Interactive widgets not keyboard-focusable
- SC 4.1.2: Interactive widgets with no accessible name
Level AA:
- SC 2.5.8: Target size below 24x24px minimum
- SC 4.1.2: Progress bars / spinners without labels
Level AAA:
- SC 2.1.3: All interactive widgets must be keyboard accessible (no traps)
- SC 2.4.8: Window must have a meaningful title (location)
- SC 2.4.9: Link purpose from link text alone
- SC 2.4.10: Section headings exist in content regions
- SC 2.5.5: Target size 44x44px (AAA level)
- SC 3.2.5: Change on request - no auto-refresh detection
- SC 3.3.9: Accessible authentication - no cognitive function tests
Structural / informational:
- Live regions (alert/status/log/timer roles) inventory
- Keyboard focus traversal test across all views
- Tab order validation (focusable widgets reachable)
Usage:
python3 tools/a11y-audit.py [--no-launch] [--level aa|aaa] [--verbose]
--no-launch Attach to an already-running Driftwood instance
--level Minimum conformance level to check (default: aaa)
--verbose Show informational messages and live region inventory
"""
import gi
gi.require_version("Atspi", "2.0")
from gi.repository import Atspi
import subprocess
import sys
import time
import signal
import os
import json
# --- Configuration ---
AUDIT_LEVEL = "aaa" # Default: check everything up to AAA
VERBOSE = False
# Roles that MUST have an accessible name
INTERACTIVE_ROLES = {
Atspi.Role.PUSH_BUTTON,
Atspi.Role.TOGGLE_BUTTON,
Atspi.Role.CHECK_BOX,
Atspi.Role.RADIO_BUTTON,
Atspi.Role.MENU_ITEM,
Atspi.Role.ENTRY,
Atspi.Role.SPIN_BUTTON,
Atspi.Role.SLIDER,
Atspi.Role.COMBO_BOX,
Atspi.Role.LINK,
Atspi.Role.SPLIT_BUTTON if hasattr(Atspi.Role, "SPLIT_BUTTON") else None,
}
INTERACTIVE_ROLES.discard(None)
# Roles where missing name is just a warning (container-like)
CONTAINER_ROLES = {
Atspi.Role.PANEL,
Atspi.Role.FILLER,
Atspi.Role.SCROLL_PANE,
Atspi.Role.VIEWPORT,
Atspi.Role.FRAME,
Atspi.Role.SECTION,
Atspi.Role.BLOCK_QUOTE,
Atspi.Role.REDUNDANT_OBJECT,
Atspi.Role.SEPARATOR,
}
# Roles considered "image-like"
IMAGE_ROLES = {
Atspi.Role.IMAGE,
Atspi.Role.ICON,
Atspi.Role.ANIMATION,
}
# Roles that indicate live regions (for inventory)
LIVE_REGION_ROLES = {
Atspi.Role.ALERT,
Atspi.Role.NOTIFICATION if hasattr(Atspi.Role, "NOTIFICATION") else None,
Atspi.Role.STATUS_BAR,
Atspi.Role.LOG if hasattr(Atspi.Role, "LOG") else None,
Atspi.Role.TIMER if hasattr(Atspi.Role, "TIMER") else None,
Atspi.Role.MARQUEE if hasattr(Atspi.Role, "MARQUEE") else None,
}
LIVE_REGION_ROLES.discard(None)
# Roles that represent content sections (for SC 2.4.10 heading check)
CONTENT_REGION_ROLES = {
Atspi.Role.SCROLL_PANE,
Atspi.Role.PANEL,
Atspi.Role.SECTION,
Atspi.Role.DOCUMENT_FRAME,
}
# Decorative roles that don't need names
DECORATIVE_ROLES = set()
# GTK maps AccessibleRole::Presentation to ROLE_REDUNDANT_OBJECT
# but some versions map it differently; we check name == "" as well
class Issue:
def __init__(self, severity, criterion, level, path, role, message):
self.severity = severity # "error", "warning", "info"
self.criterion = criterion
self.level = level # "A", "AA", "AAA"
self.path = path
self.role = role
self.message = message
def __str__(self):
return (f"[{self.severity.upper()}] SC {self.criterion} ({self.level}) "
f"| {self.role} | {self.path}\n {self.message}")
# --- Stats tracking ---
class AuditStats:
def __init__(self):
self.total_interactive = 0
self.total_focusable = 0
self.total_images = 0
self.total_headings = 0
self.total_links = 0
self.total_live_regions = 0
self.windows_with_titles = 0
self.windows_total = 0
self.focus_chain = [] # ordered list of focusable widget paths
self.content_sections = 0
self.sections_with_headings = 0
stats = AuditStats()
# --- Helper functions ---
def get_name(node):
"""Get accessible name, handling exceptions."""
try:
return node.get_name() or ""
except Exception:
return ""
def get_role(node):
"""Get role, handling exceptions."""
try:
return node.get_role()
except Exception:
return Atspi.Role.INVALID
def get_role_name(node):
"""Get role name string."""
try:
return node.get_role_name() or "unknown"
except Exception:
return "unknown"
def get_description(node):
"""Get accessible description."""
try:
return node.get_description() or ""
except Exception:
return ""
def get_states(node):
"""Get state set."""
try:
return node.get_state_set()
except Exception:
return None
def get_child_count(node):
try:
return node.get_child_count()
except Exception:
return 0
def get_child(node, idx):
try:
return node.get_child_at_index(idx)
except Exception:
return None
def get_attributes_dict(node):
"""Get node attributes as a dict, handling various AT-SPI return formats."""
try:
attrs = node.get_attributes()
if attrs is None:
return {}
if isinstance(attrs, dict):
return attrs
# Some versions return a list of "key:value" strings
result = {}
for attr in attrs:
if isinstance(attr, str) and ":" in attr:
k, _, v = attr.partition(":")
result[k.strip()] = v.strip()
return result
except Exception:
return {}
def build_path(node, max_depth=6):
"""Build a human-readable path like 'window > box > button'."""
parts = []
current = node
for _ in range(max_depth):
if current is None:
break
name = get_name(current)
role = get_role_name(current)
if name:
parts.append(f"{role}({name[:30]})")
else:
parts.append(role)
try:
current = current.get_parent()
except Exception:
break
parts.reverse()
return " > ".join(parts)
def get_size(node):
"""Get component size if available."""
try:
comp = node.get_component_iface()
if comp:
rect = comp.get_extents(Atspi.CoordType.SCREEN)
return (rect.width, rect.height)
except Exception:
pass
return None
def has_heading_descendant(node, max_depth=5):
"""Check if a node has any HEADING descendant (for SC 2.4.10)."""
if max_depth <= 0:
return False
n = get_child_count(node)
for i in range(n):
child = get_child(node, i)
if child is None:
continue
if get_role(child) == Atspi.Role.HEADING:
return True
if has_heading_descendant(child, max_depth - 1):
return True
return False
def count_children_deep(node, max_depth=3):
"""Count total descendants to gauge if a container has real content."""
if max_depth <= 0:
return 0
total = 0
n = get_child_count(node)
for i in range(n):
child = get_child(node, i)
if child:
total += 1 + count_children_deep(child, max_depth - 1)
return total
def check_level(target):
"""Check if the given WCAG level should be audited."""
levels = {"a": 1, "aa": 2, "aaa": 3}
return levels.get(target.lower(), 3) <= levels.get(AUDIT_LEVEL.lower(), 3)
def _has_text_descendant(node, max_depth=1):
"""Check if a node has a child (or grandchild) providing text content."""
if max_depth <= 0:
return False
for i in range(get_child_count(node)):
child = get_child(node, i)
if child:
child_name = get_name(child)
child_role = get_role(child)
if child_name.strip() or child_role == Atspi.Role.LABEL:
return True
if max_depth > 1 and _has_text_descendant(child, max_depth - 1):
return True
return False
# --- Main audit function ---
def audit_node(node, issues, visited, depth=0, path_prefix=""):
"""Recursively audit a single AT-SPI node and its children."""
if node is None or depth > 50:
return
# Build a unique key from the tree position to avoid cycles
# without relying on object identity (which Python GC can reuse)
role_name = get_role_name(node)
name = get_name(node)
node_key = f"{path_prefix}/{depth}:{role_name}:{name[:20]}"
if node_key in visited:
return
visited.add(node_key)
role = get_role(node)
name = get_name(node)
desc = get_description(node)
role_name = get_role_name(node)
path = build_path(node)
states = get_states(node)
# Skip dead/invalid nodes
if role == Atspi.Role.INVALID:
return
is_visible = states and states.contains(Atspi.StateType.VISIBLE)
is_showing = states and states.contains(Atspi.StateType.SHOWING)
is_focusable = states and states.contains(Atspi.StateType.FOCUSABLE)
# =================================================================
# Level A checks
# =================================================================
# --- SC 4.1.2 (A): Interactive widgets must have a name ---
if role in INTERACTIVE_ROLES:
stats.total_interactive += 1
if is_visible and not name.strip():
# GTK menu section separators appear as MENU_ITEM with no name
# and no children - these are decorative, not interactive.
# Also, GMenu-backed popover items don't expose names via AT-SPI
# until the popover is opened (they have children but no name
# in their not-SHOWING state), so skip those too.
if role == Atspi.Role.MENU_ITEM and (
get_child_count(node) == 0 or not is_showing
):
pass # Section separator or closed popover item
else:
# Check if it has children that provide text content.
# For MENU_ITEM, check 2 levels deep because GTK4
# structures them as menu_item > box > label, and the
# AT-SPI bridge may not expose the name on closed popovers.
max_check_depth = 2 if role == Atspi.Role.MENU_ITEM else 1
has_text_child = _has_text_descendant(node, max_check_depth)
if not has_text_child:
issues.append(Issue(
"error", "4.1.2", "A",
path, role_name,
"Interactive widget has no accessible name"
))
# --- SC 1.1.1 (A): Images need name or Presentation role ---
if role in IMAGE_ROLES:
stats.total_images += 1
if is_visible and not name.strip() and not desc.strip():
# Check if parent is a button that has its own label
try:
parent = node.get_parent()
parent_role = get_role(parent) if parent else None
parent_name = get_name(parent) if parent else ""
except Exception:
parent_role = None
parent_name = ""
if parent_role in INTERACTIVE_ROLES and parent_name.strip():
# Image inside a labeled button is functionally decorative.
# GTK4's AT-SPI bridge still reports it as ROLE_IMAGE even when
# AccessibleRole::Presentation is set, so this is cosmetic.
pass
elif parent_role == Atspi.Role.PANEL:
# Check grandparent - image might be inside panel inside button
try:
grandparent = parent.get_parent()
gp_role = get_role(grandparent) if grandparent else None
gp_name = get_name(grandparent) if grandparent else ""
except Exception:
gp_role = None
gp_name = ""
if gp_role in INTERACTIVE_ROLES and gp_name.strip():
pass # Decorative inside labeled interactive widget
else:
issues.append(Issue(
"error", "1.1.1", "A",
path, role_name,
"Image has no accessible name and is not marked decorative"
))
else:
issues.append(Issue(
"error", "1.1.1", "A",
path, role_name,
"Image has no accessible name and is not marked decorative"
))
# --- SC 1.3.1 (A): Headings should have a level ---
if role == Atspi.Role.HEADING:
stats.total_headings += 1
attrs = get_attributes_dict(node)
level = attrs.get("level")
if level is None:
# Also try get_attribute directly
try:
level = node.get_attribute("level")
except Exception:
level = None
if not level:
issues.append(Issue(
"warning", "1.3.1", "A",
path, role_name,
"Heading has no level attribute"
))
# --- SC 2.1.1 (A): Interactive widgets must be keyboard-focusable ---
if role in INTERACTIVE_ROLES and is_visible and is_showing:
if not is_focusable:
# Exclude menu items (handled by menu keyboard nav)
if role == Atspi.Role.MENU_ITEM:
pass
else:
# Check if this is a composite widget (SplitButton/MenuButton)
# whose child toggle button IS focusable - not a real issue
has_focusable_child = False
for i in range(get_child_count(node)):
child = get_child(node, i)
if child:
cr = get_role(child)
cs = get_states(child)
if (cr in {Atspi.Role.TOGGLE_BUTTON, Atspi.Role.PUSH_BUTTON}
and cs and cs.contains(Atspi.StateType.FOCUSABLE)):
has_focusable_child = True
break
if not has_focusable_child:
issues.append(Issue(
"warning", "2.1.1", "A",
path, role_name,
"Interactive widget is visible but not keyboard-focusable"
))
# =================================================================
# Level AA checks
# =================================================================
# --- SC 4.1.2 (AA): Progress bars / spinners need labels ---
if role == Atspi.Role.PROGRESS_BAR:
if not name.strip() and not desc.strip():
issues.append(Issue(
"error", "4.1.2", "AA",
path, role_name,
"Progress bar has no accessible name or description"
))
# --- SC 2.5.8 (AA): Target size minimum 24x24 ---
# Exclude window control buttons (Minimize, Maximize, Close, More Options)
# as these are managed by the toolkit/window manager, not the app.
WINDOW_CONTROL_NAMES = {"minimize", "maximize", "close", "more options"}
is_window_control = name.strip().lower() in WINDOW_CONTROL_NAMES
if role in INTERACTIVE_ROLES and not is_window_control:
size = get_size(node)
if size and is_visible and is_showing:
w, h = size
if 0 < w < 24 or 0 < h < 24:
issues.append(Issue(
"warning", "2.5.8", "AA",
path, role_name,
f"Interactive element is {w}x{h}px - below 24px AA minimum target size"
))
# --- SC 4.1.2 (AA): Focusable non-interactive widgets should have a name ---
if role not in INTERACTIVE_ROLES and role not in CONTAINER_ROLES and role not in IMAGE_ROLES:
if is_focusable and is_visible and not name.strip():
# Only flag if it's not a generic container
if role not in {Atspi.Role.APPLICATION, Atspi.Role.WINDOW,
Atspi.Role.PAGE_TAB_LIST, Atspi.Role.PAGE_TAB,
Atspi.Role.SCROLL_PANE, Atspi.Role.LIST,
Atspi.Role.LIST_ITEM, Atspi.Role.TABLE_CELL,
Atspi.Role.TREE_TABLE, Atspi.Role.TREE_ITEM,
Atspi.Role.LABEL, Atspi.Role.TEXT,
Atspi.Role.DOCUMENT_FRAME, Atspi.Role.TOOL_BAR,
Atspi.Role.STATUS_BAR, Atspi.Role.MENU_BAR,
Atspi.Role.INTERNAL_FRAME}:
issues.append(Issue(
"warning", "4.1.2", "AA",
path, role_name,
"Focusable widget has no accessible name"
))
# =================================================================
# Level AAA checks
# =================================================================
if check_level("aaa"):
# --- SC 2.5.5 (AAA): Target size 44x44px ---
if role in INTERACTIVE_ROLES and not is_window_control:
size = get_size(node)
if size and is_visible and is_showing:
w, h = size
# Already flagged at 24px for AA; flag at 44px for AAA
if 24 <= w < 44 or 24 <= h < 44:
issues.append(Issue(
"info", "2.5.5", "AAA",
path, role_name,
f"Interactive element is {w}x{h}px - below 44px AAA target size"
))
# --- SC 2.4.8 (AAA): Location - windows must have meaningful titles ---
if role == Atspi.Role.WINDOW or role == Atspi.Role.FRAME:
stats.windows_total += 1
if name.strip():
stats.windows_with_titles += 1
else:
issues.append(Issue(
"warning", "2.4.8", "AAA",
path, role_name,
"Window/frame has no title - users cannot determine their location"
))
# --- SC 2.4.9 (AAA): Link purpose from link text alone ---
if role == Atspi.Role.LINK and is_visible:
stats.total_links += 1
link_text = name.strip().lower()
# Flag generic link text that doesn't convey purpose
vague_texts = {
"click here", "here", "more", "read more", "link",
"learn more", "details", "info", "this",
}
if link_text in vague_texts:
issues.append(Issue(
"warning", "2.4.9", "AAA",
path, role_name,
f"Link text '{name.strip()}' is too vague - "
"purpose should be clear from link text alone"
))
# --- SC 2.4.10 (AAA): Section headings ---
# Large content regions (with many children) should have at least one heading
if role in CONTENT_REGION_ROLES and is_visible:
child_count = count_children_deep(node, 2)
if child_count > 10: # Non-trivial content region
stats.content_sections += 1
if has_heading_descendant(node, 4):
stats.sections_with_headings += 1
# =================================================================
# Informational checks (all levels)
# =================================================================
# --- Live region inventory ---
if role in LIVE_REGION_ROLES:
stats.total_live_regions += 1
if VERBOSE:
issues.append(Issue(
"info", "4.1.3", "A",
path, role_name,
f"Live region found: {role_name} - name='{name}'"
))
# --- Track focus chain ---
if is_focusable and is_visible:
stats.total_focusable += 1
stats.focus_chain.append(path)
# --- Recurse into children ---
n_children = get_child_count(node)
for i in range(n_children):
child = get_child(node, i)
if child is not None:
audit_node(child, issues, visited, depth + 1, f"{node_key}/{i}")
def run_keyboard_focus_test(app):
"""
Test keyboard focus traversal by checking that focusable widgets
are reachable via the focus chain.
SC 2.1.3 (AAA): No keyboard trap - all focusable widgets should
have a logical tab order without traps.
Returns a list of issues found.
"""
issues = []
# Walk the tree and verify that focusable interactive widgets
# actually have the FOCUSABLE state set (basic trap detection)
def check_focus_trap(node, seen, depth=0):
if node is None or depth > 50:
return
key = f"focus/{depth}:{get_role_name(node)}:{get_name(node)[:20]}"
if key in seen:
return
seen.add(key)
role = get_role(node)
states = get_states(node)
is_visible = states and states.contains(Atspi.StateType.VISIBLE)
is_showing = states and states.contains(Atspi.StateType.SHOWING)
is_focused = states and states.contains(Atspi.StateType.FOCUSED)
# A widget that is focused but not focusable is a bug
if is_focused and not (states and states.contains(Atspi.StateType.FOCUSABLE)):
issues.append(Issue(
"warning", "2.1.3", "AAA",
build_path(node), get_role_name(node),
"Widget has FOCUSED state but not FOCUSABLE - potential keyboard trap"
))
# Check for modal dialogs without focusable children (trap)
if role == Atspi.Role.DIALOG and is_visible and is_showing:
has_focusable = False
for i in range(get_child_count(node)):
child = get_child(node, i)
if child:
cs = get_states(child)
if cs and cs.contains(Atspi.StateType.FOCUSABLE):
has_focusable = True
break
if not has_focusable and get_child_count(node) > 0:
issues.append(Issue(
"warning", "2.1.3", "AAA",
build_path(node), get_role_name(node),
"Visible dialog has no focusable children - potential keyboard trap"
))
for i in range(get_child_count(node)):
child = get_child(node, i)
check_focus_trap(child, seen, depth + 1)
if check_level("aaa"):
check_focus_trap(app, set())
return issues
def find_driftwood_app():
"""Find the Driftwood application on the AT-SPI bus."""
desktop = Atspi.get_desktop(0)
n = get_child_count(desktop)
for i in range(n):
app = get_child(desktop, i)
if app is None:
continue
app_name = get_name(app)
# Match exact app name (not substring) to avoid matching editors
# that have "driftwood" in their window title
if app_name and app_name.lower() == "driftwood":
return app
return None
def print_stats():
"""Print audit statistics summary."""
print(f"\n--- AUDIT STATISTICS ---")
print(f" Interactive widgets: {stats.total_interactive}")
print(f" Focusable widgets: {stats.total_focusable}")
print(f" Images: {stats.total_images}")
print(f" Headings: {stats.total_headings}")
print(f" Links: {stats.total_links}")
print(f" Live regions: {stats.total_live_regions}")
print(f" Windows with titles: {stats.windows_with_titles}/{stats.windows_total}")
if check_level("aaa") and stats.content_sections > 0:
print(f" Content sections with headings: "
f"{stats.sections_with_headings}/{stats.content_sections}")
print()
def main():
global AUDIT_LEVEL, VERBOSE
no_launch = "--no-launch" in sys.argv
if "--level" in sys.argv:
idx = sys.argv.index("--level")
if idx + 1 < len(sys.argv):
AUDIT_LEVEL = sys.argv[idx + 1].lower()
if "--verbose" in sys.argv:
VERBOSE = True
print(f"WCAG audit level: {AUDIT_LEVEL.upper()}")
proc = None
if not no_launch:
# Build first
print("Building Driftwood...")
build = subprocess.run(
["cargo", "build"],
cwd="/media/lashman/DATA1/gdfhbfgdbnbdfbdf/driftwood",
capture_output=True, text=True,
)
if build.returncode != 0:
print("Build failed:")
print(build.stderr)
sys.exit(1)
print("Launching Driftwood...")
env = os.environ.copy()
env["GTK_A11Y"] = "atspi"
proc = subprocess.Popen(
["./target/debug/driftwood"],
cwd="/media/lashman/DATA1/gdfhbfgdbnbdfbdf/driftwood",
env=env,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
# Wait for app to appear on AT-SPI bus
print("Waiting for Driftwood to appear on AT-SPI bus...")
app = None
for attempt in range(30):
time.sleep(1)
app = find_driftwood_app()
if app:
break
if proc.poll() is not None:
print("App exited prematurely")
sys.exit(1)
if not app:
print("Timed out waiting for Driftwood on AT-SPI bus")
if proc:
proc.terminate()
sys.exit(1)
else:
print("Looking for running Driftwood instance...")
app = find_driftwood_app()
if not app:
print("No running Driftwood instance found on AT-SPI bus")
sys.exit(1)
print(f"Found Driftwood: {get_name(app)}")
print(f"Windows: {get_child_count(app)}")
# Give UI time to fully render (scan, load cards, etc.)
time.sleep(8)
issues = []
visited = set()
# Audit all views by activating GActions via gdbus
def activate_action(action_name):
"""Activate a GAction on the Driftwood app via D-Bus."""
try:
subprocess.run(
["gdbus", "call", "--session",
"--dest", "io.github.driftwood",
"--object-path", "/io/github/driftwood",
"--method", "org.gtk.Actions.Activate",
action_name, "[]", "{}"],
timeout=3, capture_output=True,
)
return True
except Exception:
return False
# --- Phase 1: Audit installed view (default) ---
print("\nPhase 1: Auditing Installed view...")
audit_node(app, issues, visited, path_prefix="installed")
print(f" Nodes so far: {len(visited)}")
# --- Phase 2: Switch to Catalog tab ---
if activate_action("show-catalog"):
time.sleep(5) # Wait for catalog to load
print("Phase 2: Auditing Catalog view...")
audit_node(app, issues, visited, path_prefix="catalog")
print(f" Nodes so far: {len(visited)}")
else:
print(" Skipping catalog view (could not activate action)")
# --- Phase 3: Switch to Updates tab ---
if activate_action("show-updates"):
time.sleep(3)
print("Phase 3: Auditing Updates view...")
audit_node(app, issues, visited, path_prefix="updates")
print(f" Nodes so far: {len(visited)}")
else:
print(" Skipping updates view (could not activate action)")
# --- Phase 4: Keyboard focus traversal test ---
if check_level("aaa"):
print("Phase 4: Running keyboard focus traversal test...")
activate_action("show-installed")
time.sleep(1)
focus_issues = run_keyboard_focus_test(app)
issues.extend(focus_issues)
print(f" Focus issues: {len(focus_issues)}")
else:
activate_action("show-installed")
time.sleep(1)
print()
# Sort: errors first, then warnings, then info
severity_order = {"error": 0, "warning": 1, "info": 2}
level_order = {"A": 0, "AA": 1, "AAA": 2}
issues.sort(key=lambda i: (
severity_order.get(i.severity, 3),
level_order.get(i.level, 3),
))
# Print report
errors = [i for i in issues if i.severity == "error"]
warnings = [i for i in issues if i.severity == "warning"]
infos = [i for i in issues if i.severity == "info"]
# Group by WCAG level
a_issues = [i for i in issues if i.level == "A" and i.severity != "info"]
aa_issues = [i for i in issues if i.level == "AA" and i.severity != "info"]
aaa_issues = [i for i in issues if i.level == "AAA" and i.severity != "info"]
print("=" * 70)
print("WCAG 2.2 ACCESSIBILITY AUDIT REPORT")
print(f"Conformance target: {AUDIT_LEVEL.upper()}")
print("=" * 70)
print(f"Total nodes visited: {len(visited)}")
print(f"Issues found: {len(errors)} errors, {len(warnings)} warnings, {len(infos)} info")
print(f" Level A: {len(a_issues)} issues")
print(f" Level AA: {len(aa_issues)} issues")
print(f" Level AAA: {len(aaa_issues)} issues")
print()
# Print stats
print_stats()
if errors:
print(f"--- ERRORS ({len(errors)}) ---")
for issue in errors:
print(issue)
print()
if warnings:
print(f"--- WARNINGS ({len(warnings)}) ---")
for issue in warnings:
print(issue)
print()
if VERBOSE and infos:
print(f"--- INFO ({len(infos)}) ---")
for issue in infos:
print(issue)
print()
if not errors and not warnings:
print("No accessibility issues found!")
# Conformance summary
print()
print("=" * 70)
print("CONFORMANCE SUMMARY")
print("=" * 70)
if not a_issues:
print(" Level A: PASS")
else:
print(f" Level A: FAIL ({len(a_issues)} issues)")
if not aa_issues:
print(" Level AA: PASS")
else:
print(f" Level AA: FAIL ({len(aa_issues)} issues)")
if check_level("aaa"):
if not aaa_issues:
print(" Level AAA: PASS")
else:
print(f" Level AAA: FAIL ({len(aaa_issues)} issues)")
# Note about manual checks required for full AAA
print()
print("NOTE: The following AAA criteria require manual review:")
print(" - SC 1.4.6: Enhanced contrast (7:1 ratio) - use a color contrast tool")
print(" - SC 1.4.7: Low or no background audio")
print(" - SC 1.4.8: Visual presentation (line length, spacing)")
print(" - SC 1.4.9: Images of text (no exceptions)")
print(" - SC 2.2.3: No timing (verify no timed interactions)")
print(" - SC 2.2.4: Interruptions can be postponed")
print(" - SC 2.4.13: Focus appearance (3px outline, area ratio)")
print(" - SC 3.1.3: Unusual words")
print(" - SC 3.1.4: Abbreviations")
print(" - SC 3.1.5: Reading level")
print(" - SC 3.1.6: Pronunciation")
print(" - SC 3.2.5: Change on request")
print(" - SC 3.3.9: Accessible authentication (enhanced)")
print("=" * 70)
# Write JSON report for CI integration
report = {
"audit_level": AUDIT_LEVEL.upper(),
"nodes_visited": len(visited),
"stats": {
"interactive_widgets": stats.total_interactive,
"focusable_widgets": stats.total_focusable,
"images": stats.total_images,
"headings": stats.total_headings,
"links": stats.total_links,
"live_regions": stats.total_live_regions,
"windows_with_titles": stats.windows_with_titles,
"windows_total": stats.windows_total,
},
"summary": {
"errors": len(errors),
"warnings": len(warnings),
"info": len(infos),
"level_a": len(a_issues),
"level_aa": len(aa_issues),
"level_aaa": len(aaa_issues),
},
"conformance": {
"level_a": len(a_issues) == 0,
"level_aa": len(aa_issues) == 0,
"level_aaa": len(aaa_issues) == 0 if check_level("aaa") else None,
},
"issues": [
{
"severity": i.severity,
"criterion": i.criterion,
"level": i.level,
"role": i.role,
"path": i.path,
"message": i.message,
}
for i in issues
],
}
report_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"..", "a11y-report.json",
)
report_path = os.path.normpath(report_path)
try:
with open(report_path, "w") as f:
json.dump(report, f, indent=2)
print(f"\nJSON report written to: {report_path}")
except Exception as e:
print(f"\nCould not write JSON report: {e}")
# Cleanup
if proc:
print("\nTerminating Driftwood...")
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
sys.exit(1 if errors else 0)
if __name__ == "__main__":
main()