This file consolidates the architectural decisions for Crossbar. Operational rules and project context still live in AGENTS.md; if there is any conflict, treat AGENTS.md as the source of truth.
- ADR-001: Unified CLI Binary (2024-12-07) [Superseded]
- ADR-002: Embedded Lua Interpreter (2024-12-07)
- ADR-003: QuickJS Fallback for JavaScript (2024-12-07) [Rejected]
- ADR-004: GNOME Desktop Integration (2024-12-07)
- ADR-005: Separated Icon for Linux (2024-12-07)
- ADR-006: Universal Synchronous API for Embedded Scripting (2024-12-07)
- ADR-007: Android System Info via /proc (2024-12-07) [Deprecated]
- ADR-008: Android Internal Plugins Directory (2024-12-08)
- ADR-009: Unified Refresh Behavior via RefreshService (2025-12-21)
- ADR-010: Android Native APIs via Method Channel (2025-12-21)
- ADR-011: Monorepo with Separate Packages (2025-12-22)
- ADR-012: Multi-Icon Tray Architecture for Linux (2025-12-23)
- ADR-013: Platform Theme Detection & Monochrome Icons (2026-02-06)
- ADR-014: Lua-First Sample Plugin System (2026-02-06)
- ADR-015: Mobile Notification & Widget Menu Architecture (2026-02-08)
Status: Superseded by ADR-011
Context:
- The project previously shipped three binaries (launcher, CLI, GUI), which complicated distribution and process spawning.
Decision:
- Merge launcher and CLI into a single
crossbarbinary; keepcrossbar-guias the GUI binary.
Consequences:
- Fewer binaries to distribute and manage.
crossbar --versionand CLI commands run without extra process hops.- GUI is still a separate Flutter binary.
Status: Accepted
Context:
- Script-based plugins (bash/python/node) depend on external interpreters and do not work on mobile.
Decision:
- Embed Lua 5.3 via
lua_dardofor a universal, pure-Dart interpreter.
Consequences:
- Lua plugins run on all platforms (desktop and mobile).
- No external dependencies required.
- Performance trade-off compared to native Node/Python.
Status: Rejected
Context:
- JavaScript plugins needed a mobile-compatible runtime.
Decision:
- Rejected
flutter_js(QuickJS) because it depends ondart:uiand breaks the pure Dart CLI build.
Consequences:
- Desktop JS plugins run via Node.
- Mobile JS plugins are not supported; Lua is the recommended cross-platform alternative.
Status: Accepted
Context:
- GNOME dock/taskbar integration was inconsistent due to mismatched application IDs and icons.
Decision:
- Standardize
APPLICATION_ID(com.verseles.crossbar) across:.desktopfile name and fields- icon name and
StartupWMClass - GTK runner WM_CLASS
Consequences:
- Correct icon association on GNOME and compatible desktops.
- Improved dock/taskbar behavior.
Status: Accepted
Context:
- The transparent icon did not render well across Linux desktop environments.
Decision:
- Generate a Linux-specific icon with rounded corners (
icon_linux.png).
Consequences:
- More consistent appearance on GNOME and similar environments.
- Icon generation automated in the build pipeline.
Status: Accepted
Context:
- Embedded Lua is synchronous; the original API was entirely async, which complicated Lua access to system data.
Decision:
- Add sync variants to core APIs and expose them via the Lua bridge.
Consequences:
- Lua plugins can use
crossbar.cpu(),crossbar.memory(), etc. synchronously. - Some calls block the Dart isolate.
Status: Deprecated (superseded by ADR-010)
Context:
- Android tightened access to
/procand/sys, breaking CPU/battery reads.
Decision:
- Read
/proc/stat,/proc/meminfo, and/sys/class/power_supplyfor Android system info.
Consequences:
- CLI remained pure Dart.
- CPU returned 0% on Android 8+.
- Battery access failed on newer Android releases.
Status: Accepted
Context:
- Plugin storage on Android required a decision between internal storage and external folders (SAF).
Decision:
- Use only internal app storage for plugins.
Consequences:
- Simple, secure implementation.
- Users cannot add plugins manually via file manager on Android.
Status: Accepted
Context:
- Refresh logic was split across services, causing inconsistent behavior between UI, tray, and scheduler.
Decision:
- Centralize refresh execution and caching in
RefreshService.
Consequences:
- Consistent refresh behavior across UI, tray, IPC, and widgets.
- Clearer ownership of plugin outputs.
Status: Accepted
Context:
/procand/sysaccess for CPU/battery was blocked by Android security.
Decision:
- Introduce
AndroidNativeBridgewith MethodChannel calls to native APIs (BatteryManager, ActivityManager), plus caching for sync access.
Consequences:
- Battery data works reliably on Android.
- CPU remains unavailable; APIs return 0.0 with a clear limitation.
- Separation keeps the CLI build pure Dart.
Status: Accepted
Context:
dart compile execould not handle conditional Flutter imports used by the CLI.
Decision:
- Create a monorepo with separate packages:
crossbar_corefor pure Dart shared logiccrossbar_clifor the CLI binary- Flutter app as the root package
Consequences:
- CLI compiles without Flutter dependencies.
- Shared models and APIs live in
crossbar_core.
Status: Accepted
Context:
- Linux tray APIs allow only a single icon per process in many environments.
Decision:
- Spawn a separate daemon process per plugin to own its own SNI icon.
Consequences:
- Multiple tray icons work on Linux (GNOME/KDE).
- Additional processes are required and capped (default 10).
- Some visual updates depend on user interaction due to SNI limitations.
Status: Accepted
Context:
- Flutter's
platformDispatcher.onPlatformBrightnessChangeddoes not fire when the user toggles light/dark mode in GNOME Settings. The initial value is read correctly, but runtime changes are not propagated by the GTK event loop. - Android and GNOME both require monochrome (white + transparent) icons for notifications and status bars.
Decision:
- Use
gsettings monitor org.gnome.desktop.interface color-schemeas an external process to detect theme changes in real time on Linux. The stdout events propagate brightness toSettingsService.detectedSystemBrightness, triggeringnotifyListeners()and aMaterialApprebuild. Flutter'sonPlatformBrightnessChangedis kept as a fallback. - Generate monochrome PNGs (white + transparency via ImageMagick
CopyOpacity) in 5 Android densities (ic_stat_crossbar.png). Reference viaAndroidNotificationDetails.icon. The foreground service Kotlin code also usesR.drawable.ic_stat_crossbar.
Consequences:
- App theme and tray icon react in real time to GNOME theme changes.
- Monochrome notification icons work correctly on Android and GNOME.
- Depends on
gsettings(available on GNOME/GTK; fails silently on KDE/others). - One additional external process on Linux (minimal overhead, event-driven).
AndroidInitializationSettingsmust use@mipmap/ic_launcher, never a custom drawable (causes silent failure).
Status: Accepted
Context:
- Crossbar supported 8 languages for sample plugins (Bash, Python, Node.js, Dart, Go, Rust, Lua, YAML). Each plugin maintained 2-8 variants, totaling ~80 files, UI complexity (language dropdown per plugin), and unnecessary assets in the bundle. All 25 plugins already had a working Lua version. Lua is the only language with an embedded interpreter (
lua_dardo), working on all platforms without external dependencies.
Decision:
- Make official samples Lua-only (remove bash/python/node/dart/go/rust/yaml variants).
- Keep the full
PluginLanguageenum and execution core intact (users can create plugins in any language). - Simplify the sample dialog UI (remove language dropdown).
- Scaffolding (
crossbar init) and marketplace continue supporting all languages.
Consequences:
- ~47 fewer files in the bundle (~43 scripts + 4 schemas).
- Dramatically simplified UI (no language dropdown).
- Smaller APK/bundle size.
- All samples work on all platforms.
- Trade-off: developers who prefer bash/python must create their own plugins.
- Marketplace and user plugin execution are not affected.
Status: Accepted
Context:
- On desktop, plugins display menus in the systray (tray icon submenus). On mobile (Android/iOS), widgets completely ignored plugin menu items, and the Settings section showed irrelevant "System Tray" options (Unified/Separate/SmartCollapse). There was no way to access links or information from menu items on mobile.
Decision:
- Replace "System Tray" settings section with "Notifications" on mobile, with a
NotificationStyleoption (Combined/Individual/Both). Move "Keep on Background" toggle from Behavior to Notifications on mobile. Desktop keeps System Tray unchanged. - Add a more_vert (three dots) button on Android widgets. Clicking opens
WidgetMenuActivity— a pure Kotlin Activity (no Flutter engine), dialog-themed (Theme.Translucent.NoTitleBar), that reads menu items from SharedPreferences and renders a programmatic bottom-sheet. Items withhrefopen browser, items withbashappear disabled ("Desktop only"). Supports inline submenus, separators, custom colors, and dark mode. - Implement
showCombinedNotification()(InboxStyle, up to 6 lines) andshowIndividualNotification()(BigTextStyle with expanded menu items). Notifications usesetGroup()for Android 7+ grouping. Stable IDs viapluginId.hashCodefor updates without duplication.
Architecture:
Widget (RemoteViews) -> PendingIntent -> WidgetMenuActivity
-> SharedPreferences -> plugin_<id> JSON -> menu[]
-> Programmatic UI (LinearLayout + ScrollView)
-> href items: Intent.ACTION_VIEW -> Browser
SchedulerService._onPluginOutput()
-> check NotificationStyle
-> combined: NotificationService.showCombinedNotification() -> InboxStyle
-> individual: NotificationService.showIndividualNotification() -> BigTextStyle
-> both: both above
PendingIntent offsets (to avoid collision):
- Container (open app):
appWidgetId - Edit:
appWidgetId + 2000 - Menu (single plugin widget):
appWidgetId + 4000 - Menu (large widget row N):
appWidgetId + 5000 + (N * 100)
Consequences:
- Plugin menu items are accessible on mobile via widgets and notifications.
WidgetMenuActivitydoes not depend on Flutter engine (instant startup).- Notifications are user-configurable (Combined/Individual/Both).
- Items with
bashare disabled on mobile (no shell available). WidgetMenuActivitybuilds UI programmatically (no XML layout) — more flexible but less standard.- Dark mode detected via
Configuration.uiMode.