Compare commits
3 Commits
99f60b8529
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6beca34f70 | ||
|
|
09ef0f48e0 | ||
|
|
58d8b44866 |
481
README.md
@@ -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
|
**Your apps. Your computer. No middleman.**
|
||||||
with a clean GNOME-native interface built for the Wayland era.
|
|
||||||
|
|
||||||
## 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
|
<br />
|
||||||
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
|
|
||||||
|
|
||||||
## Requirements
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
- GTK 4.16+
|
<br />
|
||||||
- libadwaita 1.6+
|
|
||||||
- SQLite 3
|
|
||||||
- gettext
|
|
||||||
|
|
||||||
Optional:
|
*No accounts. No tracking. No data harvested. No profit motive.*
|
||||||
- firejail (for sandboxed launches)
|
*Just a tool that helps you manage your apps, and gets out of the way.*
|
||||||
- fuse2/fuse3 (for AppImage FUSE mounting)
|
|
||||||
- appimageupdate (for delta updates)
|
|
||||||
|
|
||||||
## 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
|
```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 build
|
||||||
cargo run
|
cargo run
|
||||||
|
```
|
||||||
|
|
||||||
# System installation (uses meson)
|
### System-wide installation
|
||||||
|
|
||||||
|
```sh
|
||||||
meson setup build --prefix=/usr
|
meson setup build --prefix=/usr
|
||||||
meson compile -C build
|
meson compile -C build
|
||||||
sudo meson install -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
|
```sh
|
||||||
# Scan configured directories for AppImages
|
# Full AAA-level audit (builds and launches automatically)
|
||||||
driftwood scan
|
python3 tools/a11y-audit.py --level aaa
|
||||||
|
|
||||||
# List all known AppImages
|
# Attach to an already-running instance
|
||||||
driftwood list
|
python3 tools/a11y-audit.py --no-launch --level aaa
|
||||||
driftwood list --format json
|
|
||||||
|
|
||||||
# Inspect a specific AppImage
|
# AA level only
|
||||||
driftwood inspect ~/Applications/Firefox.AppImage
|
python3 tools/a11y-audit.py --level aa
|
||||||
|
|
||||||
# Integrate into desktop menu
|
# Verbose output with live region inventory
|
||||||
driftwood integrate ~/Applications/Firefox.AppImage
|
python3 tools/a11y-audit.py --level aaa --verbose
|
||||||
|
|
||||||
# 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Packaging
|
The tool checks for:
|
||||||
|
|
||||||
- **Flatpak**: See `build-aux/app.driftwood.Driftwood.json`
|
- Interactive widgets without accessible names (SC 4.1.2)
|
||||||
- **Arch Linux (AUR)**: See `packaging/PKGBUILD`
|
- 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.
|
||||||
|
|||||||
@@ -23,24 +23,105 @@
|
|||||||
<li>Duplicate detection and disk space analysis</li>
|
<li>Duplicate detection and disk space analysis</li>
|
||||||
<li>Firejail sandboxing support</li>
|
<li>Firejail sandboxing support</li>
|
||||||
<li>Orphaned configuration cleanup</li>
|
<li>Orphaned configuration cleanup</li>
|
||||||
|
<li>Browse and install from the AppImageHub catalog</li>
|
||||||
</ul>
|
</ul>
|
||||||
</description>
|
</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>
|
<launchable type="desktop-id">app.driftwood.Driftwood.desktop</launchable>
|
||||||
<url type="bugtracker">https://github.com/driftwood-app/driftwood/issues</url>
|
|
||||||
|
|
||||||
<developer id="app.driftwood">
|
<developer id="app.driftwood">
|
||||||
<name>Driftwood Contributors</name>
|
<name>Driftwood Contributors</name>
|
||||||
</developer>
|
</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>
|
<branding>
|
||||||
<color type="primary" scheme_preference="light">#8ff0a4</color>
|
<color type="primary" scheme_preference="light">#8ff0a4</color>
|
||||||
<color type="primary" scheme_preference="dark">#26a269</color>
|
<color type="primary" scheme_preference="dark">#26a269</color>
|
||||||
</branding>
|
</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>
|
<requires>
|
||||||
<display_length compare="ge">360</display_length>
|
<display_length compare="ge">360</display_length>
|
||||||
@@ -51,18 +132,18 @@
|
|||||||
<control>pointing</control>
|
<control>pointing</control>
|
||||||
</recommends>
|
</recommends>
|
||||||
|
|
||||||
<keywords>
|
<supports>
|
||||||
<keyword>AppImage</keyword>
|
<internet>first-run</internet>
|
||||||
<keyword>Application</keyword>
|
</supports>
|
||||||
<keyword>Manager</keyword>
|
|
||||||
<keyword>Package</keyword>
|
<provides>
|
||||||
<keyword>FUSE</keyword>
|
<binary>driftwood</binary>
|
||||||
<keyword>Wayland</keyword>
|
</provides>
|
||||||
<keyword>Security</keyword>
|
|
||||||
</keywords>
|
<translation type="gettext">app.driftwood.Driftwood</translation>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
<release version="0.1.0" date="2026-02-26">
|
<release version="0.1.0" date="2026-02-26" type="stable">
|
||||||
<description>
|
<description>
|
||||||
<p>Initial release of Driftwood with core features:</p>
|
<p>Initial release of Driftwood with core features:</p>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -75,6 +156,7 @@
|
|||||||
<li>Firejail sandbox support</li>
|
<li>Firejail sandbox support</li>
|
||||||
<li>Orphan cleanup and disk reclamation wizard</li>
|
<li>Orphan cleanup and disk reclamation wizard</li>
|
||||||
<li>CLI interface with scan, list, launch, and inspect commands</li>
|
<li>CLI interface with scan, list, launch, and inspect commands</li>
|
||||||
|
<li>AppImageHub catalog browsing and one-click install</li>
|
||||||
</ul>
|
</ul>
|
||||||
</description>
|
</description>
|
||||||
</release>
|
</release>
|
||||||
|
|||||||
11
data/icons/hicolor/scalable/apps/app.driftwood.Driftwood.svg
Normal 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 |
@@ -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 |
BIN
data/screenshots/01-catalog-loading.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
data/screenshots/02-library-grid.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
data/screenshots/03-library-list.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
data/screenshots/04-app-detail-about.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
data/screenshots/05-app-detail-screenshots.png
Normal file
|
After Width: | Height: | Size: 246 KiB |
BIN
data/screenshots/06-app-detail-integration.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
data/screenshots/07-dashboard.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
data/screenshots/08-preferences.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
data/screenshots/09-drag-and-drop.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
data/screenshots/10-catalog-browse.png
Normal file
|
After Width: | Height: | Size: 328 KiB |
BIN
data/screenshots/11-catalog-app-detail.png
Normal file
|
After Width: | Height: | Size: 256 KiB |
BIN
data/screenshots/12-catalog-category.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
@@ -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
|
|
||||||
17
meson.build
@@ -2,7 +2,7 @@ project(
|
|||||||
'driftwood',
|
'driftwood',
|
||||||
'rust',
|
'rust',
|
||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
license: 'GPL-3.0-or-later',
|
license: 'CC0-1.0',
|
||||||
meson_version: '>= 0.62.0',
|
meson_version: '>= 0.62.0',
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,12 +28,25 @@ install_data(
|
|||||||
install_dir: datadir / 'metainfo',
|
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
|
# Compile and install GSettings schema
|
||||||
install_data(
|
install_data(
|
||||||
'data' / app_id + '.gschema.xml',
|
'data' / app_id + '.gschema.xml',
|
||||||
install_dir: datadir / 'glib-2.0' / 'schemas',
|
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
|
# Build the Rust binary via Cargo
|
||||||
cargo = find_program('cargo')
|
cargo = find_program('cargo')
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ mkdir -p "$APPDIR/usr/bin"
|
|||||||
mkdir -p "$APPDIR/usr/share/applications"
|
mkdir -p "$APPDIR/usr/share/applications"
|
||||||
mkdir -p "$APPDIR/usr/share/glib-2.0/schemas"
|
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/scalable/apps"
|
||||||
|
mkdir -p "$APPDIR/usr/share/icons/hicolor/symbolic/apps"
|
||||||
|
mkdir -p "$APPDIR/usr/share/metainfo"
|
||||||
|
|
||||||
# Binary
|
# Binary
|
||||||
cp "target/release/driftwood" "$APPDIR/usr/bin/driftwood"
|
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"
|
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/"
|
glib-compile-schemas "$APPDIR/usr/share/glib-2.0/schemas/"
|
||||||
|
|
||||||
# Icon - use a placeholder SVG if no real icon exists yet
|
# Icon
|
||||||
ICON_FILE="data/icons/$APP_ID.svg"
|
ICON_FILE="data/icons/hicolor/scalable/apps/$APP_ID.svg"
|
||||||
if [ -f "$ICON_FILE" ]; then
|
if [ -f "$ICON_FILE" ]; then
|
||||||
cp "$ICON_FILE" "$APPDIR/usr/share/icons/hicolor/scalable/apps/$APP_ID.svg"
|
cp "$ICON_FILE" "$APPDIR/usr/share/icons/hicolor/scalable/apps/$APP_ID.svg"
|
||||||
else
|
else
|
||||||
@@ -56,6 +58,18 @@ else
|
|||||||
SVGEOF
|
SVGEOF
|
||||||
fi
|
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
|
# Check for linuxdeploy
|
||||||
LINUXDEPLOY="${LINUXDEPLOY:-linuxdeploy}"
|
LINUXDEPLOY="${LINUXDEPLOY:-linuxdeploy}"
|
||||||
if ! command -v "$LINUXDEPLOY" &>/dev/null; then
|
if ! command -v "$LINUXDEPLOY" &>/dev/null; then
|
||||||
@@ -77,20 +91,20 @@ if ! command -v "$LINUXDEPLOY" &>/dev/null; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Check for GTK plugin
|
# Check for GTK plugin
|
||||||
GTK_PLUGIN="${LINUXDEPLOY_PLUGIN_GTK:-}"
|
GTK_PLUGIN_ARG=""
|
||||||
if [ -z "$GTK_PLUGIN" ]; then
|
if [ -x "./linuxdeploy-plugin-gtk.sh" ]; then
|
||||||
if [ -x "./linuxdeploy-plugin-gtk.sh" ]; then
|
export DEPLOY_GTK_VERSION=4
|
||||||
export DEPLOY_GTK_VERSION=4
|
GTK_PLUGIN_ARG="--plugin gtk"
|
||||||
else
|
echo "GTK4 plugin found - libraries will be bundled."
|
||||||
echo ""
|
else
|
||||||
echo "Warning: linuxdeploy-plugin-gtk not found."
|
echo ""
|
||||||
echo "GTK4 libraries will not be bundled."
|
echo "Warning: linuxdeploy-plugin-gtk not found."
|
||||||
echo "The AppImage may only work on systems with GTK4 and libadwaita installed."
|
echo "GTK4 libraries will not be bundled."
|
||||||
echo ""
|
echo "The AppImage may only work on systems with GTK4 and libadwaita installed."
|
||||||
echo "Download the plugin from:"
|
echo ""
|
||||||
echo " https://github.com/nickvdp/linuxdeploy-plugin-gtk"
|
echo "Download the plugin from:"
|
||||||
echo ""
|
echo " https://github.com/linuxdeploy/linuxdeploy-plugin-gtk"
|
||||||
fi
|
echo ""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "=== Building AppImage ==="
|
echo "=== Building AppImage ==="
|
||||||
@@ -102,6 +116,7 @@ export GSETTINGS_SCHEMA_DIR="$APPDIR/usr/share/glib-2.0/schemas"
|
|||||||
--appdir "$APPDIR" \
|
--appdir "$APPDIR" \
|
||||||
--desktop-file "$APPDIR/usr/share/applications/$APP_ID.desktop" \
|
--desktop-file "$APPDIR/usr/share/applications/$APP_ID.desktop" \
|
||||||
--icon-file "$APPDIR/usr/share/icons/hicolor/scalable/apps/$APP_ID.svg" \
|
--icon-file "$APPDIR/usr/share/icons/hicolor/scalable/apps/$APP_ID.svg" \
|
||||||
|
$GTK_PLUGIN_ARG \
|
||||||
--output appimage
|
--output appimage
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use adw::subclass::prelude::*;
|
|||||||
use gtk::gio;
|
use gtk::gio;
|
||||||
use std::cell::OnceCell;
|
use std::cell::OnceCell;
|
||||||
|
|
||||||
use crate::config::{APP_ID, VERSION};
|
use crate::config::APP_ID;
|
||||||
use crate::window::DriftwoodWindow;
|
use crate::window::DriftwoodWindow;
|
||||||
|
|
||||||
mod imp {
|
mod imp {
|
||||||
@@ -118,28 +118,8 @@ impl DriftwoodApplication {
|
|||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// About action
|
self.add_action_entries([quit_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"]);
|
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,7 +184,6 @@ impl DriftwoodWindow {
|
|||||||
let section3 = gio::Menu::new();
|
let section3 = gio::Menu::new();
|
||||||
section3.append(Some(&i18n("Preferences")), Some("win.preferences"));
|
section3.append(Some(&i18n("Preferences")), Some("win.preferences"));
|
||||||
section3.append(Some(&i18n("Keyboard Shortcuts")), Some("win.show-shortcuts"));
|
section3.append(Some(&i18n("Keyboard Shortcuts")), Some("win.show-shortcuts"));
|
||||||
section3.append(Some(&i18n("About Driftwood")), Some("app.about"));
|
|
||||||
menu.append_section(None, §ion3);
|
menu.append_section(None, §ion3);
|
||||||
|
|
||||||
// Library view (contains header bar, search, grid/list, empty state)
|
// Library view (contains header bar, search, grid/list, empty state)
|
||||||
|
|||||||
@@ -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()
|
|
||||||