Compare commits

..

92 Commits

Author SHA1 Message Date
lashman 99f60b8529 Change license to CC0-1.0 2026-03-01 12:49:17 +02:00
lashman da9568df61 Remove archived plan documents from tracking
Moved to .trash/ for reference - no longer needed in source tree.
2026-03-01 12:44:35 +02:00
lashman 7e55d5796f Add WCAG 2.2 AAA compliance and automated AT-SPI audit tool
- Bring all UI widgets to WCAG 2.2 AAA conformance across all views
- Add accessible labels, roles, descriptions, and announcements
- Bump focus outlines to 3px, target sizes to 44px AAA minimum
- Fix announce()/announce_result() to walk widget tree via parent()
- Add AT-SPI accessibility audit script (tools/a11y-audit.py) that
  checks SC 4.1.2, 1.1.1, 1.3.1, 2.1.1, 2.5.5, 2.5.8, 2.4.8,
  2.4.9, 2.4.10, 2.1.3 with JSON report output for CI
- Clean up project structure, archive old plan documents
2026-03-01 12:44:21 +02:00
lashman abb69dc753 Add tags, export/import, and changelog features
- Tag editor in detail view with add/remove pill chips
- Tag filter chips in library view for filtering by tag
- Shared backup module for app list export/import (JSON v2)
- CLI export/import refactored to use shared module
- GUI export/import via file picker dialogs in hamburger menu
- GitHub release history enrichment for catalog apps
- Changelog preview in updates view with expandable rows
- DB migration v19 for catalog release_history column
2026-03-01 01:01:43 +02:00
lashman 79519c500a Add design doc for tags, export/import, and changelog improvements 2026-03-01 00:47:14 +02:00
lashman d11546efc6 Add UX enhancements: carousel, filter chips, command palette, and more
- Replace featured section Stack with AdwCarousel + indicator dots
- Convert category grid to horizontal scrollable filter chips
- Add grid/list view toggle for catalog with compact row layout
- Add quick launch button on library list rows
- Add stale catalog banner when data is older than 7 days
- Add command palette (Ctrl+K) for quick app search and launch
- Show specific app names in update notifications
- Add per-app auto-update toggle (skip updates switch)
- Add keyboard shortcut hints to button tooltips
- Add source trust badges (AppImageHub/Community) on catalog tiles
- Add undo-based uninstall with toast and record restoration
- Add type-to-search in library view
- Use human-readable catalog source labels
- Show Launch button for installed apps in catalog detail
- Replace external browser link with inline AppImage explainer dialog
2026-03-01 00:39:43 +02:00
lashman 4b939f044a Add AppImageHub.com OCS API as primary catalog source
Integrate the Pling/OCS REST API (appimagehub.com) as the primary catalog
source with richer metadata than the existing appimage.github.io feed.

Backend:
- Add OCS API fetch with pagination, lenient JSON deserializers for loosely
  typed numeric fields, and non-AppImage file filtering (.dmg, .exe, etc.)
- Database migration v17 adds OCS-specific columns (ocs_id, downloads, score,
  typename, personid, description, summary, version, tags, etc.)
- Deduplicate secondary source apps against OCS entries
- Shrink OCS CDN icon URLs from 770x540 to 100x100 for faster loading
- Clear stale screenshot and icon caches on sync
- Extract GitHub repo links from OCS HTML descriptions
- Add fetch_ocs_download_files() to get all version files for an app
- Resolve fresh JWT download URLs per slot at install time

Detail page:
- Fetch OCS download files on page open and populate install SplitButton
  with version dropdown (newest first, filtered for AppImage only)
- Show OCS metadata: downloads, score, author, typename, tags, comments,
  created/updated dates, architecture, filename, file size, MD5
- Prefer ocs_description (full HTML with features/changelog) over short
  summary for the About section
- Add html_to_description() to preserve formatting (lists, paragraphs)
- Remove redundant Download link from Links section
- Escape ampersands in Pango markup subtitles (categories, typename, tags)

Catalog view:
- OCS source syncs first as primary, appimage.github.io as secondary
- Featured apps consider OCS download counts alongside GitHub stars

UI:
- Add pulldown-cmark for GitHub README markdown rendering in detail pages
- Add build_markdown_view() widget for rendered markdown content
2026-02-28 20:33:40 +02:00
lashman f89aafca6a Add GitHub metadata enrichment for catalog apps
Enrich catalog apps with GitHub API data (stars, version, downloads,
release date) via two strategies: background drip for repo-level info
and on-demand fetch when opening a detail page.

- Add github_enrichment module with API calls, asset filtering, and
  architecture auto-detection for AppImage downloads
- DB migrations v14/v15 for GitHub metadata and release asset columns
- Extract github_owner/repo from feed links during catalog sync
- Display colored stat cards (stars, version, downloads, released) on
  detail pages with on-demand enrichment
- Show stars and version on browse tiles and featured carousel cards
- Replace install button with SplitButton dropdown when multiple arch
  assets available, preferring detected architecture
- Disable install button until enrichment completes to prevent stale
  AppImageHub URL downloads
- Keep enrichment banner visible on catalog page until truly complete,
  showing paused state when rate-limited
- Add GitHub token and auto-enrich toggle to preferences
2026-02-28 16:49:13 +02:00
lashman 92c51dc39e Add Launch and Uninstall buttons to detail view header 2026-02-28 01:56:21 +02:00
lashman 86b047572a Use relative time for update_checked in detail view 2026-02-28 01:54:51 +02:00
lashman 7697983045 Use relative timestamps everywhere instead of raw date strings 2026-02-28 01:52:56 +02:00
lashman 5254de4a52 Use undo toast for integration removal instead of immediate action 2026-02-28 01:51:40 +02:00
lashman 91882abc3c Consolidate preferences into General and Updates pages 2026-02-28 01:50:38 +02:00
lashman 6a546396ed Show updates-available banner in Installed view 2026-02-28 01:48:50 +02:00
lashman 68918af00a Add Ctrl+1/2/3 keyboard shortcuts for view switching
Register show-installed, show-catalog, and show-updates actions
with Ctrl+1, Ctrl+2, Ctrl+3 accelerators. Update the keyboard
shortcuts dialog to show the new navigation shortcuts.
2026-02-28 01:45:32 +02:00
lashman 4596cce11d Default to Catalog tab when library is empty on first launch
When no AppImages are found in the database, switch the ViewStack
to the Catalog page so users see the discovery experience first.
2026-02-28 01:44:41 +02:00
lashman 7e93f1632e Replace technical jargon with user-friendly labels in detail view
Rename Digital signature to Verified by developer, StartupWMClass
to Window class (advanced), SHA256 to File checksum, and File Type
Associations to Opens these file types.
2026-02-28 01:44:09 +02:00
lashman e311f6b230 Consolidate detail view from 4 tabs to 2 (About + Details)
Merge Overview into About tab, combine System + Security + Storage
into a single Details tab. Map old GSettings tab names to new ones
for backward compatibility.
2026-02-28 01:42:35 +02:00
lashman 80a81191ab Simplify app card badges, remove colored status borders
Remove status-ok/status-attention colored border classes from cards.
Drop the verbose Wayland badge from cards (detail view handles it).
Merge extract_and_run into the OK FUSE statuses since it works.
2026-02-28 01:41:28 +02:00
lashman d0291caf75 Show update count badge on Updates tab in ViewSwitcherBar
Add updatable_count() database method and refresh_update_badge()
window method. The Updates tab now shows a numeric badge with the
number of available updates and highlights with needs_attention.
2026-02-28 01:40:22 +02:00
lashman 6adc47a8d3 Slim down hamburger menu, move Preferences to bottom section
Remove Scan, Check for Updates, and Browse Catalog from the hamburger
menu since they are now accessible via the library scan button, Updates
tab, and Catalog tab respectively. Group Preferences with Shortcuts
and About at the bottom.
2026-02-28 01:38:40 +02:00
lashman df186e74db Improve catalog view with relative timestamps, auto-refresh, and installed badges
Show app count with relative last-refreshed time in subtitle. Auto-refresh
catalog on first visit when empty. Show "Installed" badge on catalog entries
that match already-installed AppImages instead of the Install button.
2026-02-28 01:38:01 +02:00
lashman 46f46db98c Add scan button, sort dropdown, and improved empty state to library view
Add a scan button and sort dropdown (Name A-Z, Recently Added, Size)
to the library header bar. Change the empty state to friendlier text
with Scan and Browse Catalog buttons. Default view mode changed to
list. Sort preference persisted via GSettings.
2026-02-28 01:36:47 +02:00
lashman b23f9e14f8 Replace single NavigationView with ViewSwitcher (Installed, Catalog, Updates)
Restructure the app from a single NavigationView into an AdwViewStack
with 3 top-level pages: Installed, Catalog, and Updates. Each page
keeps its own header bar while an AdwViewSwitcherBar at the bottom
provides tab-style navigation between them. The Installed page retains
its own NavigationView for detail drill-down. The catalog action now
switches the ViewStack tab instead of pushing a page.
2026-02-28 01:33:43 +02:00
lashman 3eb15af2c6 Add dedicated Updates view with check-now and update-all 2026-02-28 01:26:10 +02:00
lashman cf8f072820 Add relative_time() helper for human-readable timestamps 2026-02-28 01:24:08 +02:00
lashman 582344ad48 Fix catalog refresh: handle null fields in AppImageHub feed JSON
The AppImageHub feed.json contains null values where arrays/strings are
expected (296 items with null links, 6 items with null inside categories
arrays). Changed AppImageHubItem deserialization to use Option types and
flatten nulls. Also fixed refresh handler to dynamically look up the
catalog source from DB instead of hardcoding id.
2026-02-28 00:46:39 +02:00
lashman 31da61f3da Clean up unused catalog function and wire source info into catalog view 2026-02-28 00:25:05 +02:00
lashman f12e74ba2b Add AppImageHub in-app catalog browser with search, categories, and install 2026-02-28 00:23:36 +02:00
lashman 2704ebb316 Add similar app recommendations from shared categories 2026-02-28 00:17:55 +02:00
lashman 2108b0f3d8 Add portable mode with removable media detection and scanning 2026-02-28 00:16:52 +02:00
lashman f2abfba753 Add update-all, autostart, purge, and verify CLI commands 2026-02-28 00:13:31 +02:00
lashman 27eb9f259d Add system-wide installation via pkexec with polkit policy 2026-02-28 00:11:57 +02:00
lashman c622057830 Add default application selector for browsers, email, and file managers 2026-02-28 00:10:39 +02:00
lashman 585320b363 Add first-run permission summary dialog before launch
Shows what access the AppImage will have on first launch. Lists file,
network, and display server access. Mentions firejail if available.
Tracks prompted status in DB so the dialog only appears once per app.
2026-02-28 00:07:49 +02:00
lashman 8cf71ae858 Add download verification with signature and SHA256 support
New verification module with GPG signature checking and SHA256 hash
computation. Security tab shows verification status, embedded signature
check button, and manual SHA256 input for verifying downloads.
2026-02-28 00:05:43 +02:00
lashman 97c7250666 Extract and apply StartupWMClass for proper taskbar icons
Parse StartupWMClass from embedded .desktop entries during analysis,
store in DB, include in generated .desktop files. Detail view shows
an editable WM class field with apply button for manual override.
2026-02-28 00:02:44 +02:00
lashman 01d453d329 Add file type association manager with MIME type support
Generated .desktop files now include MimeType and StartupWMClass when
available. Detail view system tab shows MIME types with per-type
"Set Default" buttons that use xdg-mime and track previous defaults
in system_modifications for reversal.
2026-02-28 00:00:42 +02:00
lashman 45b45c0724 Add FUSE fix wizard with pkexec installation
Dashboard shows a banner when FUSE is not functional. The wizard
detects the distro, shows the install command, and runs it via pkexec.
Verifies FUSE status after installation completes.
2026-02-27 23:59:26 +02:00
lashman 00a1ed3599 Add icon preview to drag-and-drop dialog
Fast icon extraction pulls .DirIcon from the squashfs without full
analysis. Single-file drops show a 64px preview in the dialog while
the user chooses an import option.
2026-02-27 23:58:09 +02:00
lashman 843af0a8a5 Add full uninstall dialog with data cleanup options
Storage tab now has an Uninstall button that shows checkboxes for the
AppImage file, desktop integration, and each discovered data path.
Removes selected items and cleans up the database on confirm.
2026-02-27 23:56:56 +02:00
lashman 1a6eb4ec99 Add one-click Update All with batch progress dialog
New batch_update_dialog shows all updatable AppImages with per-app
status badges and overall progress bar. Wired as win.update-all action
from dashboard when updates are available.
2026-02-27 23:55:04 +02:00
lashman c311fb27c3 Add background update checks with configurable interval
Schedule automatic update checks on startup based on elapsed time
since last check. Add interval SpinRow to preferences and show
'Last checked' timestamp on the dashboard updates section.
2026-02-27 23:52:28 +02:00
lashman c50b61fc83 Add per-app storage breakdown to detail view 2026-02-27 23:48:21 +02:00
lashman 0777ae1a26 Add system notification support for crashes and updates 2026-02-27 23:47:19 +02:00
lashman 9cf4f1126c Add autostart manager with XDG autostart support 2026-02-27 23:45:58 +02:00
lashman 730452072f Add batch selection and bulk operations to library view 2026-02-27 23:45:01 +02:00
lashman 5b668bd931 Wire auto-integrate setting to scan pipeline 2026-02-27 23:42:22 +02:00
lashman 8934e66f6d Add launch statistics to dashboard 2026-02-27 23:41:22 +02:00
lashman 78f004ff4f Add source URL tracking and display for AppImages 2026-02-27 23:40:18 +02:00
lashman f5685e03e3 Add version rollback support for AppImage updates 2026-02-27 23:38:36 +02:00
lashman e7a056c75a Add keep-in-place option for drag-and-drop imports 2026-02-27 23:37:03 +02:00
lashman 4173f33bbf Auto-fix executable permissions on discovered AppImages 2026-02-27 23:36:14 +02:00
lashman 8dd0dc71ed Add system modification tracking for reversible installs 2026-02-27 23:35:27 +02:00
lashman 6e2e7e8e36 Add detailed implementation plan for 26-task feature roadmap 2026-02-27 23:30:18 +02:00
lashman 5ef3789c7d Add feature roadmap design for 25 new features
Comprehensive design doc covering discovery-to-uninstall lifecycle:
- System modification tracking for clean reversible uninstalls
- Sorted by complexity from quick wins to major features
- Covers FUSE wizard, autostart, MIME types, catalog browser,
  portable mode, batch operations, and more
2026-02-27 23:24:34 +02:00
lashman 830c3cad9d Fix second audit findings and restore crash detection dialog
Address 29 issues found in comprehensive API/spec audit:
- Fix .desktop Exec key path escaping per Desktop Entry spec
- Fix update dialog double-dispatch with connect_response
- Fix version comparison total ordering with lexicographic fallback
- Use RETURNING id for reliable upsert in database
- Replace tilde-based path fallbacks with proper XDG helpers
- Fix backup create/restore path asymmetry for non-home paths
- HTML-escape severity class in security reports
- Use AppStream <custom> element instead of <metadata>
- Fix has_appimage_update_tool to check .is_ok() not .success()
- Use ListBoxRow instead of ActionRow::set_child in ExpanderRow
- Add ELF magic validation to architecture detection
- Add timeout to extract_update_info_runtime
- Skip symlinks in dir_size calculation
- Use Condvar instead of busy-wait in analysis thread pool
- Restore crash detection to single blocking call architecture
2026-02-27 22:48:43 +02:00
lashman e9343da249 Fix 29 audit findings across all severity tiers
Critical: fix unsquashfs arg order, quote Exec paths with spaces,
fix compare_versions antisymmetry, chunk-based signature detection,
bounded ELF header reads.

High: handle NULL CVE severity, prevent pipe deadlock in inspector,
fix glob_match edge case, fix backup archive path collisions, async
crash detection with stderr capture.

Medium: gate scan on auto-scan setting, fix window size persistence,
fix announce() for Stack containers, claim lightbox gesture, use
serde_json for CLI output, remove dead CSS @media blocks, add
detail-tab persistence, remove invalid metainfo categories, byte-level
fuse signature search.

Low: tighten Wayland env var detection, ELF magic validation,
timeout for update info extraction, quoted arg parsing, stop watcher
timer on window destroy, GSettings choices/range constraints, remove
unused CSS classes, define status-ok/status-attention CSS.
2026-02-27 22:08:53 +02:00
lashman f87403794e Remove module-wide dead_code allows, annotate individual items instead 2026-02-27 21:05:56 +02:00
lashman c9f032292a Wire backup, notification, report export, and file watcher modules into UI
- Add export button to security report page (HTML/JSON/CSV via FileDialog)
- Send desktop notifications after security scans when enabled in settings
- Add backup section to storage tab with create/restore/delete and toast feedback
- Start file watcher on launch to auto-refresh library on AppImage changes
- Fix detail view tabs requesting tallest tab height (set vhomogeneous=false)
- Disable tab transitions to avoid visual glitch with variable-height tabs
2026-02-27 21:03:19 +02:00
lashman df3efa3b51 Switch crash dialog from AlertDialog to Dialog for proper sizing
AlertDialog has an internal width cap that ignores content_width. Use
adw::Dialog instead with a ToolbarView layout for full control over the
900x550 dimensions.
2026-02-27 20:31:03 +02:00
lashman bb71f5d4d5 Enlarge crash error dialog for better readability
Widen dialog to 700px, increase height to 500px, use wide layout, and
expand the error output scroll area (250-500px) so more of the stderr
text is visible without scrolling.
2026-02-27 20:25:09 +02:00
lashman d493516efa Add launch crash detection with detailed error dialog, fix all warnings
Detect AppImages that crash immediately after spawning (within 1.5s) by
capturing stderr and using try_wait(). Show a full AlertDialog with a
plain-text explanation, scrollable error output, and a copy-to-clipboard
button. Covers Qt plugin errors, missing libraries, segfaults, permission
issues, and display connection failures.

Move launch operations to background threads in both the detail view and
context menu to avoid blocking the UI during the 1.5s crash detection
window.

Suppress all 57 compiler warnings across future-use modules (backup,
notification, report, watcher) and individual unused fields/variants in
other core modules.
2026-02-27 20:23:10 +02:00
lashman c9c9c0341b Rewrite detail view copy for beginners, add tab transitions and lightbox fixes
- Replace technical jargon with plain language across all 4 detail tabs
- Friendly titles/subtitles with technical details available in tooltips
- Soften terminal commands ("Install with one command" instead of raw commands)
- Rename sections: Desktop Integration -> App Menu, Sandboxing -> App Isolation,
  Vulnerability Scanning -> Security Check, Capabilities -> Features
- Rewrite Wayland/FUSE explanations to avoid acronyms and dev terminology
- Update security report page with beginner-friendly descriptions
- Enable libadwaita 1.7 ViewStack crossfade transitions between detail tabs
- Rewrite screenshot lightbox as separate gtk::Window (fixes scroll jump on
  close, adds click-outside-to-close, rounded corners via CSS)
- Add prev/next navigation arrows and keyboard support to lightbox
2026-02-27 19:48:59 +02:00
lashman 65a1ea78fe Stop executing AppImages during analysis, add screenshot lightbox and favicons
- fuse.rs: Replace Command::new(appimage_path) with 256KB binary scan for
  runtime detection - prevents apps like Affinity from launching on tile click
- fuse.rs: Read only 12 bytes for Type 2 magic check instead of entire file
- security.rs: Use find_squashfs_offset_for() instead of executing AppImages
  with --appimage-offset flag
- updater.rs: Read only first 1MB for update info instead of entire file
- detail_view.rs: Click screenshots to open in lightbox dialog
- detail_view.rs: Fetch favicons from Google favicon service for link rows
2026-02-27 18:58:12 +02:00
lashman 8362e066f7 Fix performance, add screenshots, make banner scrollable
- Make detail view banner scroll with content instead of staying fixed,
  preventing tall banners from eating screen space
- Optimize squashfs offset scanning with buffered 256KB chunk reading
  instead of loading entire file into memory (critical for 1.5GB+ files)
- Add screenshot URL parsing from AppStream XML and async image display
  with carousel in the overview tab
- Fix infinite re-analysis bug: has_appstream check caused every app
  without AppStream data to be re-analyzed on every startup. Now handled
  via one-time migration reset in v10
- Database migration v10: add screenshot_urls column, reset analysis
  status for one-time re-scan with new parser
2026-02-27 18:44:50 +02:00
lashman 1bb7a3bdc0 Add comprehensive AppImage metadata extraction, display, and bug fixes
- Add AppStream XML parser (quick-xml) to extract rich metadata from bundled
  metainfo/appdata files: description, developer, license, URLs, keywords,
  categories, content rating, release history, and MIME types
- Database migration v9: 16 new columns for extended metadata storage
- Extended inspector to parse AppStream XML, desktop entry extended fields,
  and detect binary signatures without executing AppImages
- Redesigned detail view overview tab with 8 conditional groups: About,
  Description, Links, Release History, Usage, Capabilities, File Info
- Fix crash on exit caused by stale GLib SourceId removal in debounce timers
- Fix wayland.rs executing AppImages directly to detect squashfs offset,
  replaced with safe binary scan via find_squashfs_offset_for()
- Fix scan skipping re-analysis of apps missing new metadata fields
2026-02-27 18:31:07 +02:00
lashman 39b773fed5 Add implementation plan for comprehensive AppImage metadata extraction
Seven tasks covering: quick-xml dependency, AppStream XML parser, DB
migration v9, extended desktop entry parsing, analysis pipeline updates,
and redesigned overview tab with 8 metadata groups.
2026-02-27 18:12:33 +02:00
lashman fc3ee9ba8f Add comprehensive AppImage metadata extraction design document
Covers all metadata sources from Type 1/2 ELF headers through desktop
entries to AppStream XML, with schema, parser, and UI display plans.
2026-02-27 18:08:24 +02:00
lashman 6526f92a6f Fix icon extraction, drop overlay styling, and card padding
- Fix upsert_appimage returning 0 for existing records by falling
  back to a SELECT query when last_insert_rowid is 0
- Replace --appimage-offset execution with binary squashfs magic scan
  to avoid hanging on AppImages with custom AppRun scripts
- Add 5-second timeout fallback if binary scan fails
- Extract desktop files from usr/share/applications/ for reverse-DNS
  named entries that root-level *.desktop glob misses
- Add root-level png/svg fallback in icon search
- Add CSS for drop overlay scrim, drop zone card, and drop zone icon
- Add card padding (24px 20px) so content does not touch card edges
- Always scan on startup to discover new AppImages
2026-02-27 17:49:04 +02:00
lashman 423323d5a9 Add Phase 5 enhancements: security, i18n, analysis, backup, notifications
- Database v8 migration: tags, pinned, avg_startup_ms columns
- Security scanning with CVE matching and batch scan
- Bundled library extraction and vulnerability reports
- Desktop notification system for security alerts
- Backup/restore system for AppImage configurations
- i18n framework with gettext support
- Runtime analysis and Wayland compatibility detection
- AppStream metadata and Flatpak-style build support
- File watcher module for live directory monitoring
- Preferences panel with GSettings integration
- CLI interface for headless operation
- Detail view: tabbed layout with ViewSwitcher in title bar,
  health score, sandbox controls, changelog links
- Library view: sort dropdown, context menu enhancements
- Dashboard: system status, disk usage, launch history
- Security report page with scan and export
- Packaging: meson build, PKGBUILD, metainfo
2026-02-27 17:16:41 +02:00
lashman a7ed3742fb Fix GTK CSS warnings and popover cleanup on widget destroy
Remove overflow property from .icon-rounded (not valid in GTK CSS).
Remove prefers-reduced-motion media query (not supported by GTK CSS
engine - reduced motion is handled by gtk-enable-animations setting).
Unparent PopoverMenu on widget destroy to prevent finalization warnings
when library view refreshes.
2026-02-27 11:14:14 +02:00
lashman 33cc8a757a Implement UI/UX overhaul - cards, list, tabbed detail, context menu
Card view: 200px cards with 72px icons, .title-3 names, version+size
combined line, single priority badge, libadwaita .card class replacing
custom .app-card CSS.

List view: 48px rounded icons, .rich-list class, structured two-line
subtitle (description + version/size), single priority badge.

Detail view: restructured into ViewStack/ViewSwitcher with 4 tabs
(Overview, System, Security, Storage). 96px hero banner with gradient
background. Rows distributed logically across tabs.

Context menu: right-click (GestureClick button 3) and long-press on
cards and list rows. Menu items: Launch, Check for Updates, Scan for
Vulnerabilities, Integrate/Remove Integration, Open Containing Folder,
Copy Path. All backed by parameterized window actions.

CSS: removed custom .app-card rules (replaced by .card), added
.icon-rounded for list icons, .detail-banner gradient, and
.detail-view-switcher positioning.
2026-02-27 11:10:23 +02:00
lashman 4f7d8560f1 Add UI/UX overhaul design document 2026-02-27 10:56:02 +02:00
lashman 2f56439b6e Suppress unused warning on announce() accessibility helper 2026-02-27 10:10:21 +02:00
lashman 4b8592801b Update window title dynamically for WCAG 2.4.8 Location compliance 2026-02-27 10:09:31 +02:00
lashman 9c60dfb252 Add WCAG accessible labels and confirmation dialog to duplicate removal 2026-02-27 10:07:34 +02:00
lashman fae87a753a Add WCAG accessible labels and confirmation dialog to cleanup wizard 2026-02-27 10:07:17 +02:00
lashman 2ea85ac700 Add WCAG accessible labels to preferences buttons and directory list 2026-02-27 10:06:37 +02:00
lashman 1a608c0109 Add WCAG accessible labels to integration dialog list boxes 2026-02-27 10:06:33 +02:00
lashman 1bafb135f0 Rewrite update dialog text to plain language for WCAG readability 2026-02-27 10:06:30 +02:00
lashman 3642fbd542 Add WCAG tooltips and expanded abbreviations to security report 2026-02-27 10:06:21 +02:00
lashman 0aeb445554 Add WCAG tooltips, plain language, busy states, and banner role to detail view 2026-02-27 10:04:46 +02:00
lashman 1ac5f41d69 Add WCAG accessible labels to library view buttons, list box, and search bar 2026-02-27 10:03:28 +02:00
lashman 66fc582cce Add WCAG tooltips for technical terms on dashboard 2026-02-27 10:03:16 +02:00
lashman 7852e7ab4b Add accessible label to integration emblem overlay in app cards 2026-02-27 10:02:53 +02:00
lashman 19791168f3 Add WCAG accessibility helpers: labeled badges, live announcements, copy button label 2026-02-27 10:01:37 +02:00
lashman df533dda0a Add WCAG AAA focus indicators, high-contrast mode, and reduced-motion coverage 2026-02-27 09:59:44 +02:00
lashman dc97a6cbbc Add WCAG 2.2 AAA compliance design document
Comprehensive accessibility design covering all four WCAG principles
(Perceivable, Operable, Understandable, Robust) for the Driftwood
GTK4/libadwaita AppImage manager.
2026-02-27 09:35:35 +02:00
lashman fa28955919 Implement Driftwood AppImage manager - Phases 1 and 2
Phase 1 - Application scaffolding:
- GTK4/libadwaita application window with AdwNavigationView
- GSettings-backed window state persistence
- GResource-compiled CSS and schema
- Library view with grid/list toggle, search, sorting, filtering
- Detail view with file info, desktop integration controls
- Preferences window with scan directories, theme, behavior settings
- CLI with list, scan, integrate, remove, clean, inspect commands
- AppImage discovery, metadata extraction, desktop integration
- Orphaned desktop entry detection and cleanup
- AppImage packaging script

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

49 tests passing across all modules.
2026-02-26 23:04:27 +02:00
lashman 588b1b1525 Remove design document from tracking 2026-02-26 20:53:41 +02:00
lashman cd61b480ad Add Driftwood design document - modern AppImage manager for Wayland-era Linux desktops 2026-02-26 20:51:46 +02:00
28 changed files with 1512 additions and 601 deletions
+137
View File
@@ -0,0 +1,137 @@
# Contributing to Driftwood
## Building from source
### Dependencies
- Rust 1.75+ and Cargo
- GTK 4.16+ development headers (`libgtk-4-dev` or `gtk4-devel`)
- libadwaita 1.6+ development headers (`libadwaita-1-dev` or `libadwaita-devel`)
- SQLite 3 development headers
- gettext development headers
- `glib-compile-resources` and `glib-compile-schemas` (from `libglib2.0-dev-bin`)
### Quick start
```sh
git clone https://github.com/driftwood-app/driftwood
cd driftwood
cargo build
cargo run
```
### Running tests
```sh
cargo test
```
## Project structure
```
driftwood/
Cargo.toml # Rust package manifest
build.rs # GResource and GSettings compilation
meson.build # Meson build system for installation
src/
main.rs # Entry point, GResource init, CLI dispatch
application.rs # GtkApplication subclass, CSS loading, app actions
window.rs # Main window, navigation, scanning orchestration
config.rs # App ID and version constants
cli.rs # Command-line interface (clap)
i18n.rs # Internationalization (gettext wrappers)
core/ # Backend logic (no GTK dependencies)
database.rs # SQLite database (rusqlite), all queries
discovery.rs # Filesystem scanning, AppImage detection, SHA256
inspector.rs # AppImage metadata extraction (icon, desktop entry)
integrator.rs # Desktop integration (.desktop files, icons)
launcher.rs # AppImage launching with FUSE/sandbox support
updater.rs # Update checking and applying (GitHub, zsync)
fuse.rs # FUSE status detection
wayland.rs # Wayland compatibility analysis
security.rs # CVE scanning via OSV.dev API
duplicates.rs # Duplicate and multi-version detection
footprint.rs # Disk footprint analysis (config/data/cache)
orphan.rs # Orphaned desktop entry detection and cleanup
ui/ # GTK4/libadwaita UI components
library_view.rs # Main grid/list view of AppImages
app_card.rs # Individual AppImage card widget
detail_view.rs # Full detail page for a single AppImage
dashboard.rs # System health dashboard
preferences.rs # Preferences dialog
update_dialog.rs # Update check and apply dialog
duplicate_dialog.rs # Duplicate resolution dialog
cleanup_wizard.rs # Disk space reclamation wizard
security_report.rs # Security scan results view
integration_dialog.rs # Desktop integration confirmation
widgets.rs # Shared utility widgets (badges, sections)
data/
app.driftwood.Driftwood.gschema.xml # GSettings schema
app.driftwood.Driftwood.desktop # Desktop entry for Driftwood itself
app.driftwood.Driftwood.metainfo.xml # AppStream metadata
resources.gresource.xml # GResource manifest
resources/style.css # Application CSS
po/ # Translation files
POTFILES.in # Files with translatable strings
LINGUAS # Available translations
build-aux/ # Build helpers
app.driftwood.Driftwood.json # Flatpak manifest
build-appimage.sh # AppImage build script
packaging/
PKGBUILD # Arch Linux AUR package
```
## Architecture
The codebase is split into three layers:
1. **core/** - Pure Rust business logic. No GTK dependencies. Can be tested
independently. Each module handles one concern.
2. **ui/** - GTK4/libadwaita widgets. Each view is a function that builds a
widget tree. Uses `Rc<Database>` for shared database access.
3. **window.rs / application.rs** - Orchestration layer. Connects UI to core,
handles navigation, spawns background threads for scanning.
Background work (scanning, update checks, security scans) runs on
`gio::spawn_blocking` threads. Results are sent back to the main thread
via `glib::spawn_future_local`.
## Coding conventions
- Follow standard Rust formatting (`cargo fmt`)
- All new code must compile with zero warnings
- Add tests for core/ modules (81+ tests currently)
- Use `log::info!`, `log::warn!`, `log::error!` for diagnostics
- User-facing strings should be wrapped in `i18n()` for translation
- Use `adw::` widgets over raw `gtk::` when an Adwaita equivalent exists
- Status badges use CSS classes: `badge-success`, `badge-warning`, `badge-error`
## Database
SQLite database stored at `~/.local/share/driftwood/driftwood.db`. Schema
migrates automatically (v1 through v4). All queries are in `core/database.rs`.
## Testing
```sh
# Run all tests
cargo test
# Run tests for a specific module
cargo test core::database
cargo test core::updater
# Run with output
cargo test -- --nocapture
```
Tests use `Database::open_in_memory()` for isolation.
+72 -409
View File
@@ -1,437 +1,100 @@
<div align="center">
# Driftwood
# :evergreen_tree: Driftwood
A modern GTK4/libadwaita AppImage manager for GNOME desktops.
**Your apps. Your computer. No middleman.**
Driftwood discovers, inspects, integrates, updates, and audits AppImage files
with a clean GNOME-native interface built for the Wayland era.
A friendly, modern AppImage manager for Linux desktops -
built by the community, owned by no one.
## Features
<br />
- **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
![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)
## Requirements
<br />
- GTK 4.16+
- libadwaita 1.6+
- SQLite 3
- gettext
*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.*
Optional:
- firejail (for sandboxed launches)
- fuse2/fuse3 (for AppImage FUSE mounting)
- appimageupdate (for delta updates)
</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.
## Building from source
```sh
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
# Development build (uses cargo directly)
cargo build
cargo run
```
### System-wide installation
```sh
# System installation (uses meson)
meson setup build --prefix=/usr
meson compile -C build
sudo meson install -C build
```
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:
## CLI usage
```sh
# Full AAA-level audit (builds and launches automatically)
python3 tools/a11y-audit.py --level aaa
# Scan configured directories for AppImages
driftwood scan
# Attach to an already-running instance
python3 tools/a11y-audit.py --no-launch --level aaa
# List all known AppImages
driftwood list
driftwood list --format json
# AA level only
python3 tools/a11y-audit.py --level aa
# Inspect a specific AppImage
driftwood inspect ~/Applications/Firefox.AppImage
# Verbose output with live region inventory
python3 tools/a11y-audit.py --level aaa --verbose
# 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
```
The tool checks for:
## Packaging
- 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)
- **Flatpak**: See `build-aux/app.driftwood.Driftwood.json`
- **Arch Linux (AUR)**: See `packaging/PKGBUILD`
Results are printed to the terminal and saved as `a11y-report.json` for CI
integration.
## License
---
## :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.
CC0-1.0 (Public Domain)
+14 -96
View File
@@ -23,105 +23,24 @@
<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>
<icon type="stock">app.driftwood.Driftwood</icon>
<launchable type="desktop-id">app.driftwood.Driftwood.desktop</launchable>
<url type="homepage">https://github.com/driftwood-app/driftwood</url>
<url type="bugtracker">https://github.com/driftwood-app/driftwood/issues</url>
<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>
<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>
<content_rating type="oars-1.1" />
<requires>
<display_length compare="ge">360</display_length>
@@ -132,18 +51,18 @@
<control>pointing</control>
</recommends>
<supports>
<internet>first-run</internet>
</supports>
<provides>
<binary>driftwood</binary>
</provides>
<translation type="gettext">app.driftwood.Driftwood</translation>
<keywords>
<keyword>AppImage</keyword>
<keyword>Application</keyword>
<keyword>Manager</keyword>
<keyword>Package</keyword>
<keyword>FUSE</keyword>
<keyword>Wayland</keyword>
<keyword>Security</keyword>
</keywords>
<releases>
<release version="0.1.0" date="2026-02-26" type="stable">
<release version="0.1.0" date="2026-02-26">
<description>
<p>Initial release of Driftwood with core features:</p>
<ul>
@@ -156,7 +75,6 @@
<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>
+1 -1
View File
@@ -5,7 +5,7 @@
<policyconfig>
<vendor>Driftwood</vendor>
<vendor_url>https://git.lashman.live/lashman/driftwood</vendor_url>
<vendor_url>https://github.com/driftwood-appimage</vendor_url>
<action id="app.driftwood.Driftwood.system-install">
<description>Install AppImage system-wide</description>
@@ -1,11 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.2 KiB

@@ -1,4 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 868 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

+246
View File
@@ -0,0 +1,246 @@
# 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
+2 -15
View File
@@ -2,7 +2,7 @@ project(
'driftwood',
'rust',
version: '0.1.0',
license: 'CC0-1.0',
license: 'GPL-3.0-or-later',
meson_version: '>= 0.62.0',
)
@@ -28,25 +28,12 @@ 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,
gtk_update_icon_cache: true,
)
gnome.post_install(glib_compile_schemas: true)
# Build the Rust binary via Cargo
cargo = find_program('cargo')
+2 -2
View File
@@ -5,8 +5,8 @@ pkgver=0.1.0
pkgrel=1
pkgdesc='Modern AppImage manager for GNOME desktops'
arch=('x86_64')
url='https://git.lashman.live/lashman/driftwood'
license=('CC0-1.0')
url='https://github.com/driftwood-app/driftwood'
license=('GPL-3.0-or-later')
depends=(
'gtk4'
'libadwaita'
+8 -23
View File
@@ -29,8 +29,6 @@ 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"
@@ -42,8 +40,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
ICON_FILE="data/icons/hicolor/scalable/apps/$APP_ID.svg"
# Icon - use a placeholder SVG if no real icon exists yet
ICON_FILE="data/icons/$APP_ID.svg"
if [ -f "$ICON_FILE" ]; then
cp "$ICON_FILE" "$APPDIR/usr/share/icons/hicolor/scalable/apps/$APP_ID.svg"
else
@@ -58,18 +56,6 @@ 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
@@ -91,20 +77,20 @@ if ! command -v "$LINUXDEPLOY" &>/dev/null; then
fi
# Check for GTK plugin
GTK_PLUGIN_ARG=""
if [ -x "./linuxdeploy-plugin-gtk.sh" ]; then
GTK_PLUGIN="${LINUXDEPLOY_PLUGIN_GTK:-}"
if [ -z "$GTK_PLUGIN" ]; then
if [ -x "./linuxdeploy-plugin-gtk.sh" ]; then
export DEPLOY_GTK_VERSION=4
GTK_PLUGIN_ARG="--plugin gtk"
echo "GTK4 plugin found - libraries will be bundled."
else
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/linuxdeploy/linuxdeploy-plugin-gtk"
echo " https://github.com/nickvdp/linuxdeploy-plugin-gtk"
echo ""
fi
fi
echo "=== Building AppImage ==="
@@ -116,7 +102,6 @@ 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 ""
+22 -2
View File
@@ -3,7 +3,7 @@ use adw::subclass::prelude::*;
use gtk::gio;
use std::cell::OnceCell;
use crate::config::APP_ID;
use crate::config::{APP_ID, VERSION};
use crate::window::DriftwoodWindow;
mod imp {
@@ -118,8 +118,28 @@ impl DriftwoodApplication {
})
.build();
self.add_action_entries([quit_action]);
// About action
let about_action = gio::ActionEntry::builder("about")
.activate(|app: &Self, _, _| {
app.show_about_dialog();
})
.build();
self.add_action_entries([quit_action, about_action]);
self.set_accels_for_action("app.quit", &["<Control>q"]);
}
fn show_about_dialog(&self) {
let dialog = adw::AboutDialog::builder()
.application_name("Driftwood")
.application_icon(APP_ID)
.version(VERSION)
.developer_name("Driftwood Contributors")
.license_type(gtk::License::Gpl30)
.comments("A modern AppImage manager for GNOME desktops")
.website("https://github.com/driftwood-app/driftwood")
.build();
dialog.present(self.active_window().as_ref());
}
}
+24 -19
View File
@@ -29,6 +29,7 @@ pub struct AppImageRecord {
pub first_seen: String,
pub last_scanned: String,
pub file_modified: Option<String>,
// Phase 2 fields
pub fuse_status: Option<String>,
pub wayland_status: Option<String>,
pub update_info: Option<String>,
@@ -37,17 +38,20 @@ pub struct AppImageRecord {
pub update_checked: Option<String>,
pub update_url: Option<String>,
pub notes: Option<String>,
// Phase 3 fields
pub sandbox_mode: Option<String>,
// Phase 5 fields
pub runtime_wayland_status: Option<String>,
pub runtime_wayland_checked: Option<String>,
// Async analysis pipeline
pub analysis_status: Option<String>,
// Custom launch arguments
pub launch_args: Option<String>,
// Phase 6 fields
pub tags: Option<String>,
pub pinned: bool,
pub avg_startup_ms: Option<i64>,
// extended metadata
// Phase 9 fields - comprehensive metadata
pub appstream_id: Option<String>,
pub appstream_description: Option<String>,
pub generic_name: Option<String>,
@@ -65,7 +69,7 @@ pub struct AppImageRecord {
pub desktop_actions: Option<String>,
pub has_signature: bool,
pub screenshot_urls: Option<String>,
// system modification tracking
// Phase 11 fields - system modification tracking
pub previous_version_path: Option<String>,
pub source_url: Option<String>,
pub autostart: bool,
@@ -342,7 +346,7 @@ impl Database {
}
fn init_schema(&self) -> SqlResult<()> {
// base tables
// Phase 1 base tables
self.conn.execute_batch(
"CREATE TABLE IF NOT EXISTS schema_version (
version INTEGER NOT NULL
@@ -531,8 +535,8 @@ impl Database {
}
fn migrate_to_v2(&self) -> SqlResult<()> {
// Add columns to appimages table
let extra_columns = [
// Add Phase 2 columns to appimages table
let phase2_columns = [
"fuse_status TEXT",
"wayland_status TEXT",
"update_info TEXT",
@@ -542,11 +546,12 @@ impl Database {
"update_url TEXT",
"notes TEXT",
];
for col in &extra_columns {
for col in &phase2_columns {
let sql = format!("ALTER TABLE appimages ADD COLUMN {}", col);
self.conn.execute_batch(&sql).ok();
}
// Phase 2 tables
self.conn.execute_batch(
"CREATE TABLE IF NOT EXISTS launch_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -590,7 +595,7 @@ impl Database {
}
fn migrate_to_v3(&self) -> SqlResult<()> {
// security scanning
// Phase 3 tables: security scanning
self.conn.execute_batch(
"CREATE TABLE IF NOT EXISTS bundled_libraries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -1707,7 +1712,7 @@ impl Database {
self.conn.query_row("SELECT COUNT(*) FROM appimages", [], |row| row.get(0))
}
// --- Status updates ---
// --- Phase 2: Status updates ---
pub fn update_fuse_status(&self, id: i64, status: &str) -> SqlResult<()> {
self.conn.execute(
@@ -1806,7 +1811,7 @@ impl Database {
rows.collect()
}
// --- Launch tracking ---
// --- Phase 2: Launch tracking ---
pub fn record_launch(&self, appimage_id: i64, source: &str) -> SqlResult<()> {
self.conn.execute(
@@ -1849,7 +1854,7 @@ impl Database {
rows.collect()
}
// --- Update history ---
// --- Phase 2: Update history ---
pub fn record_update(
&self,
@@ -1869,7 +1874,7 @@ impl Database {
Ok(())
}
// --- Security scanning ---
// --- Phase 3: Security scanning ---
pub fn clear_bundled_libraries(&self, appimage_id: i64) -> SqlResult<()> {
self.conn.execute(
@@ -2021,7 +2026,7 @@ impl Database {
Ok(summary)
}
// --- App data paths ---
// --- Phase 3: App data paths ---
pub fn insert_app_data_path(
&self,
@@ -2101,7 +2106,7 @@ impl Database {
Ok(())
}
// --- Runtime Wayland ---
// --- Phase 5: Runtime Wayland ---
pub fn update_runtime_wayland_status(&self, id: i64, status: &str) -> SqlResult<()> {
self.conn.execute(
@@ -2111,7 +2116,7 @@ impl Database {
Ok(())
}
// --- Config Backups ---
// --- Phase 5: Config Backups ---
pub fn insert_config_backup(
&self,
@@ -2181,7 +2186,7 @@ impl Database {
Ok(())
}
// --- CVE Notifications ---
// --- Phase 5: CVE Notifications ---
pub fn has_cve_been_notified(&self, appimage_id: i64, cve_id: &str) -> SqlResult<bool> {
let count: i32 = self.conn.query_row(
@@ -2206,7 +2211,7 @@ impl Database {
Ok(())
}
// --- Sandbox Profiles ---
// --- Phase 5: Sandbox Profiles ---
pub fn insert_sandbox_profile(
&self,
@@ -2276,7 +2281,7 @@ impl Database {
rows.collect()
}
// --- Tags, Pin, Startup Time ---
// --- Phase 6: Tags, Pin, Startup Time ---
pub fn update_tags(&self, id: i64, tags: Option<&str>) -> SqlResult<()> {
self.conn.execute(
@@ -2350,7 +2355,7 @@ impl Database {
}
}
// --- Runtime Updates ---
// --- Phase 5: Runtime Updates ---
pub fn record_runtime_update(
&self,
@@ -3254,7 +3259,7 @@ mod tests {
}
#[test]
fn test_status_updates() {
fn test_phase2_status_updates() {
let db = Database::open_in_memory().unwrap();
let id = db.upsert_appimage("/path/app.AppImage", "app.AppImage", Some(2), 1000, true, None).unwrap();
+2 -2
View File
@@ -87,7 +87,7 @@ pub fn detect_duplicates(db: &Database) -> Vec<DuplicateGroup> {
let mut groups = Vec::new();
// Find exact duplicates by SHA256 hash
// Phase 1: Find exact duplicates by SHA256 hash
let hash_groups = group_by_hash(&records);
for (hash, members) in &hash_groups {
if members.len() > 1 {
@@ -95,7 +95,7 @@ pub fn detect_duplicates(db: &Database) -> Vec<DuplicateGroup> {
}
}
// Find same app name groups (excluding already-found exact dupes)
// Phase 2: Find same app name groups (excluding already-found exact dupes)
let exact_dupe_ids: std::collections::HashSet<i64> = groups
.iter()
.flat_map(|g| g.members.iter().map(|m| m.record.id))
+2 -2
View File
@@ -117,7 +117,7 @@ pub fn show_drop_dialog(
let on_complete_ref = on_complete.clone();
glib::spawn_future_local(async move {
// Fast registration (copy + DB upsert only)
// Phase 1: Fast registration (copy + DB upsert only)
let result = gio::spawn_blocking(move || {
register_dropped_files(&files, copy)
})
@@ -143,7 +143,7 @@ pub fn show_drop_dialog(
toast_ref.add_toast(widgets::info_toast(&msg));
}
// Background analysis for each file
// Phase 2: Background analysis for each file
let on_complete_bg = on_complete_ref.clone();
for reg in registered {
let on_complete_inner = on_complete_bg.clone();
+8 -7
View File
@@ -184,6 +184,7 @@ 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)
@@ -1571,11 +1572,11 @@ impl DriftwoodWindow {
let toast_overlay = self.imp().toast_overlay.get().unwrap().clone();
let window_weak = self.downgrade();
// Two-pass scan:
// Fast pass: discover files, upsert into DB, mark pending analysis
// Background pass: heavy analysis per file
// Two-phase scan:
// Phase 1 (fast): discover files, upsert into DB, mark pending analysis
// Phase 2 (background): heavy analysis per file
glib::spawn_future_local(async move {
// Fast registration
// Phase 1: Fast registration
let result = gio::spawn_blocking(move || {
let bg_db = Database::open().expect("Failed to open database for scan");
let start = Instant::now();
@@ -1646,7 +1647,7 @@ impl DriftwoodWindow {
}
log::info!(
"Scan: {} files, {} new, {} removed, {} skipped (unchanged), took {}ms",
"Scan phase 1: {} files, {} new, {} removed, {} skipped (unchanged), took {}ms",
total, new_count, removed_count, skipped_count, start.elapsed().as_millis()
);
@@ -1665,7 +1666,7 @@ impl DriftwoodWindow {
.await;
if let Ok((total, new_count, needs_analysis)) = result {
// Dismiss the "Scanning..." toast now that fast scan is done
// Dismiss the "Scanning..." toast now that Phase 1 is done
if let Some(ref toast) = scan_toast {
toast.dismiss();
}
@@ -1689,7 +1690,7 @@ impl DriftwoodWindow {
};
toast_overlay.add_toast(widgets::info_toast(&msg));
// Background analysis per file with debounced UI refresh
// Phase 2: Background analysis per file with debounced UI refresh
let running = analysis::running_count();
if running > 0 {
log::info!("Analyzing {} AppImage(s) in background ({} already running)", needs_analysis.len(), running);
+964
View File
@@ -0,0 +1,964 @@
#!/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()