Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,19 @@ Quando terminar as tarefas solicitadas faça as seguintes etapas:

---

## 3. Arquitetura de Execução (Dual-Binary)
## 3. Arquitetura de Execução (Dual-Binary + Monorepo)

O projeto compila **2 binários** para resolver problemas de dependência (GTK) e UX:
O projeto usa **monorepo** com 2 pacotes internos e compila **2 binários**:

**Pacotes (`packages/`):**

- `crossbar_core`: APIs e modelos Dart puro compartilhados
- `crossbar_cli`: CLI executável, depende de crossbar_core

**Binários:**

1. **`crossbar` (CLI + Launcher)**:
- Fonte: `bin/crossbar.dart` → `lib/cli/cli_handler.dart`
- Fonte: `packages/crossbar_cli/bin/crossbar.dart`
- Função: CLI unificado + launcher. Sem args ou com `gui` → lança GUI. Com args CLI → executa comando.
- Comandos: `crossbar cpu`, `crossbar --version`, `crossbar gui`
2. **`crossbar-gui` (Flutter App)**:
Expand Down Expand Up @@ -352,9 +359,9 @@ Se a context7 não estiver disponível no sistema, faça o seguinte:

> Decisões arquiteturais importantes que impactam todo o projeto.

### ADR-001: Unified CLI Binary (2024-12-07)
### ADR-001: Unified CLI Binary (2024-12-07) ⚠️ SUPERSEDED by ADR-011

**Status**: ✅ Accepted
**Status**: ⚠️ Superseded
**Context**: Originalmente havia 3 binários: `crossbar` (launcher), `crossbar-cli` e `crossbar-gui`. Isso causava complexidade na distribuição e spawning de processos.
**Decision**: Unificar launcher e CLI em um único `crossbar`. O GUI permanece separado como `crossbar-gui`.
**Consequences**:
Expand All @@ -364,6 +371,20 @@ Se a context7 não estiver disponível no sistema, faça o seguinte:
- `crossbar --version` funciona diretamente
- `crossbar gui` lança a GUI em modo detached

**Superseded**: Ver ADR-011 para a arquitetura atual.

### ADR-011: Monorepo com Pacotes Separados (2025-12-22)

**Status**: ✅ Accepted
**Context**: O ADR-001 unificou CLI e launcher, mas `dart compile exe` falha com imports condicionais `if (dart.library.ui)`. O código CLI dependia transitivamente de Flutter via `plugin_manager.dart`.
**Decision**: Criar monorepo com 3 pacotes: `crossbar_core` (APIs Dart puro), `crossbar_cli` (CLI executável), projeto Flutter principal.
**Consequences**:

- CLI compila isoladamente sem dependências Flutter
- Código compartilhado em `packages/crossbar_core` evita duplicação
- Config values do CLI lidos de JSON plaintext (`~/.crossbar/config/<plugin>.json`)
- Makefile: `cd packages/crossbar_cli && dart compile exe bin/crossbar.dart`

### ADR-002: Embedded Lua Interpreter (2024-12-07)

**Status**: ✅ Accepted
Expand Down
33 changes: 30 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,34 @@
.PHONY: all coverage linux macos windows android clean test analyze setup-linux setup-macos setup-windows mix icons \
install uninstall \
install uninstall precommit \
docker-build docker-shell docker-test docker-linux podman-build podman-shell podman-test podman-linux

# Pre-commit verification sequence (AGENTS.md compliance)
# Runs: analyze → coverage → linux build → android build
precommit:
@echo "══════════════════════════════════════════════════════════════"
@echo " PRECOMMIT VERIFICATION (AGENTS.md)"
@echo "══════════════════════════════════════════════════════════════"
@echo ""
@echo "Step 1/4: Static Analysis"
@echo "──────────────────────────────────────────────────────────────"
$(MAKE) analyze
@echo ""
@echo "Step 2/4: Tests with Coverage (target: 35-60%)"
@echo "──────────────────────────────────────────────────────────────"
$(MAKE) coverage
@echo ""
@echo "Step 3/4: Linux Build"
@echo "──────────────────────────────────────────────────────────────"
$(MAKE) linux
@echo ""
@echo "Step 4/4: Android Build"
@echo "──────────────────────────────────────────────────────────────"
$(MAKE) android
@echo ""
@echo "══════════════════════════════════════════════════════════════"
@echo " ✅ PRECOMMIT PASSED - Safe to commit!"
@echo "══════════════════════════════════════════════════════════════"

# Paths
LINUX_BUNDLE = build/linux/x64/release/bundle
MACOS_BUNDLE = build/macos/Build/Products/Release/crossbar.app/Contents/MacOS
Expand Down Expand Up @@ -34,8 +61,8 @@ linux:
flutter build linux --release
@echo "Setting up unified architecture..."
mv $(LINUX_BUNDLE)/crossbar $(LINUX_BUNDLE)/crossbar-gui
@echo "Compiling unified CLI..."
dart compile exe bin/crossbar.dart -o $(LINUX_BUNDLE)/crossbar
@echo "Compiling unified CLI from packages/crossbar_cli..."
cd packages/crossbar_cli && dart compile exe bin/crossbar.dart -o ../../$(LINUX_BUNDLE)/crossbar
@echo "Copying desktop integration files..."
cp linux/com.verseles.crossbar.desktop $(LINUX_BUNDLE)/
cp assets/icons/icon_linux.png $(LINUX_BUNDLE)/crossbar.png
Expand Down
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ Crossbar includes **24 example plugins** in 6 languages:
```
crossbar/
├── lib/
│ ├── core/ # Core plugin system
│ ├── core/ # Core plugin system (Flutter)
│ │ ├── plugin_manager.dart # Discovery & lifecycle
│ │ ├── script_runner.dart # Execution engine
│ │ ├── output_parser.dart # BitBar/JSON parser
Expand All @@ -429,8 +429,17 @@ crossbar/
│ │ └── widget_service.dart # Home screen widget updates
│ ├── ui/ # User interface
│ └── l10n/ # 10 languages
├── bin/
│ └── crossbar.dart # CLI entry point (75+ commands)
├── packages/ # Monorepo packages
│ ├── crossbar_core/ # Pure Dart shared APIs & models
│ │ └── lib/src/
│ │ ├── core/ # Shared core utilities
│ │ ├── models/ # Plugin, Config models
│ │ └── api/ # System, Network, Media APIs
│ └── crossbar_cli/ # Pure Dart CLI package
│ ├── bin/crossbar.dart # CLI entry point
│ └── lib/src/
│ ├── core/ # CLI-specific plugin manager
│ └── commands/ # 75+ CLI command handlers
├── plugins/ # Example plugins
├── test/ # 116 tests (>90% coverage)
└── .github/workflows/ # CI/CD pipelines
Expand Down
121 changes: 93 additions & 28 deletions lib/core/output_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,46 +75,65 @@ class OutputParser {
final menu = <MenuItem>[];
var inMenu = false;

// Stack to track parent items at each depth level
// Index 0 = root menu, Index 1 = first submenu level, etc.
final parentStack = <List<MenuItem>>[menu];

for (var i = 1; i < lines.length; i++) {
final line = lines[i];

// Check for separator (exactly ---) - but not submenu indicator (--)
if (line.trim() == '---') {
inMenu = true;
if (!inMenu) {
// First --- marks the start of the menu section
inMenu = true;
} else {
// Subsequent --- add visual separators to current level
parentStack.last.add(MenuItem.separator());
}
continue;
}

if (!inMenu) continue;

final trimmedLine = line.trim();
if (trimmedLine.isEmpty) continue;

if (trimmedLine.contains('|')) {
final parts = trimmedLine.split('|');
final itemText = parts[0].trim();
String? bash;
String? href;
String? itemColor;

for (var k = 1; k < parts.length; k++) {
final attr = parts[k].trim();
if (attr.startsWith('bash=')) {
bash = attr.substring(5);
} else if (attr.startsWith('href=')) {
href = attr.substring(5);
} else if (attr.startsWith('color=')) {
itemColor = attr.substring(6);
// Parse indent level and get actual content
final indentInfo = _parseIndentedLine(line);
final depth = indentInfo.depth;
final content = indentInfo.content;

if (content.isEmpty) continue;

// Parse menu item from content
final item = _parseMenuItemFromLine(content);

// Adjust parent stack to correct depth
// depth 0 = root menu, depth 1 = submenu of last root item, etc.
while (parentStack.length > depth + 1) {
parentStack.removeLast();
}

// If we need to go deeper, ensure parent has submenu
while (parentStack.length < depth + 1) {
final lastList = parentStack.last;
if (lastList.isNotEmpty) {
final lastItem = lastList.last;
// Initialize submenu if needed
if (lastItem.submenu == null) {
// Create a new item with submenu
final newItem = lastItem.copyWith(submenu: <MenuItem>[]);
lastList[lastList.length - 1] = newItem;
parentStack.add(newItem.submenu!);
} else {
parentStack.add(lastItem.submenu!);
}
} else {
// Cannot add child to empty parent, add to root
break;
}

menu.add(MenuItem(
text: itemText,
bash: bash,
href: href,
color: itemColor,
));
} else {
menu.add(MenuItem(text: trimmedLine));
}

// Add item to current level
parentStack.last.add(item);
}

return PluginOutput(
Expand All @@ -126,6 +145,52 @@ class OutputParser {
);
}

/// Parses BitBar-style indented line (-- prefix indicates submenu level)
/// Returns the depth level and the actual content without the prefix.
static ({int depth, String content}) _parseIndentedLine(String line) {
var depth = 0;
var current = line;

// Count leading -- pairs (each -- is one level)
while (current.startsWith('--')) {
depth++;
current = current.substring(2);
}

return (depth: depth, content: current.trim());
}

/// Parses a single menu item line (already stripped of -- prefix)
static MenuItem _parseMenuItemFromLine(String line) {
if (line.contains('|')) {
final parts = line.split('|');
final itemText = parts[0].trim();
String? bash;
String? href;
String? itemColor;

for (var k = 1; k < parts.length; k++) {
final attr = parts[k].trim();
if (attr.startsWith('bash=')) {
bash = attr.substring(5);
} else if (attr.startsWith('href=')) {
href = attr.substring(5);
} else if (attr.startsWith('color=')) {
itemColor = attr.substring(6);
}
}

return MenuItem(
text: itemText,
bash: bash,
href: href,
color: itemColor,
);
} else {
return MenuItem(text: line);
}
}

static ({String icon, String? text}) _parseIconAndText(String input) {
if (input.isEmpty) {
return (icon: '', text: null);
Expand Down
6 changes: 6 additions & 0 deletions lib/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1061,6 +1061,12 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'No plugins available. Install some plugins first.'**
String get noPluginsAvailable;

/// No description provided for @widgetUpdateNote.
///
/// In en, this message translates to:
/// **'Widgets update every 15 min in background. Open the app for instant updates.'**
String get widgetUpdateNote;
}

class _AppLocalizationsDelegate
Expand Down
14 changes: 9 additions & 5 deletions lib/l10n/app_localizations_ar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -497,18 +497,22 @@ class AppLocalizationsAr extends AppLocalizations {
'Crossbar - نظام إضافات عالمي\n\nحقوق الطبع والنشر (C) 2025\n\nهذا البرنامج هو برنامج حر: يمكنك إعادة توزيعه و/أو تعديله بموجب شروط رخصة GNU Affero العمومية التي نشرتها مؤسسة البرمجيات الحرة، إما الإصدار 3 من الرخصة، أو (حسب اختيارك) أي إصدار لاحق.\n\nيتم توزيع هذا البرنامج على أمل أن يكون مفيدًا، ولكن بدون أي ضمان؛ حتى بدون الضمان الضمني للتسويق أو الملاءمة لغرض معين. راجع رخصة GNU Affero العمومية لمزيد من التفاصيل.\n\nيجب أن تكون قد تلقيت نسخة من رخصة GNU Affero العمومية مع هذا البرنامج. إن لم تفعل، انظر <https://www.gnu.org/licenses/>.';

@override
String get widgetConfiguration => 'Widget Configuration';
String get widgetConfiguration => 'إعداد الأداة';

@override
String get selectPluginsForWidget => 'Select plugins to display';
String get selectPluginsForWidget => 'اختر الإضافات للعرض';

@override
String get selectOnePlugin => 'Select one plugin';
String get selectOnePlugin => 'اختر إضافة واحدة';

@override
String get selectPlugins => 'Select plugins';
String get selectPlugins => 'اختر الإضافات';

@override
String get noPluginsAvailable =>
'No plugins available. Install some plugins first.';
'لا توجد إضافات متاحة. قم بتثبيت بعض الإضافات أولاً.';

@override
String get widgetUpdateNote =>
'يتم تحديث الأدوات كل 15 دقيقة في الخلفية. افتح التطبيق للتحديثات الفورية.';
}
14 changes: 9 additions & 5 deletions lib/l10n/app_localizations_bn.dart
Original file line number Diff line number Diff line change
Expand Up @@ -499,18 +499,22 @@ class AppLocalizationsBn extends AppLocalizations {
'Crossbar - সার্বজনীন প্লাগইন সিস্টেম\n\nকপিরাইট (C) 2025\n\nএই প্রোগ্রামটি বিনামূল্যে সফ্টওয়্যার: আপনি এটি Free Software Foundation দ্বারা প্রকাশিত GNU Affero General Public License-এর শর্তাবলীর অধীনে পুনরায় বিতরণ এবং/অথবা সংশোধন করতে পারেন, হয় লাইসেন্সের সংস্করণ 3, অথবা (আপনার পছন্দে) পরবর্তী কোন সংস্করণ।\n\nএই প্রোগ্রামটি এই আশায় বিতরণ করা হয় যে এটি উপকারী হবে, কিন্তু কোন ওয়ারেন্টি ছাড়া; এমনকি ব্যবসায়িকতা বা বিশেষ উদ্দেশ্যে মানানসই হওয়ার অন্তর্নিহিত ওয়ারেন্টিও ছাড়া। আরও বিস্তারিত জানতে GNU Affero General Public License দেখুন।\n\nআপনার এই প্রোগ্রামের সাথে GNU Affero General Public License-এর একটি কপি পাওয়া উচিত ছিল। যদি না পান, <https://www.gnu.org/licenses/> দেখুন।';

@override
String get widgetConfiguration => 'Widget Configuration';
String get widgetConfiguration => 'উইজেট কনফিগারেশন';

@override
String get selectPluginsForWidget => 'Select plugins to display';
String get selectPluginsForWidget => 'প্রদর্শনের জন্য প্লাগইন নির্বাচন করুন';

@override
String get selectOnePlugin => 'Select one plugin';
String get selectOnePlugin => 'একটি প্লাগইন নির্বাচন করুন';

@override
String get selectPlugins => 'Select plugins';
String get selectPlugins => 'প্লাগইন নির্বাচন করুন';

@override
String get noPluginsAvailable =>
'No plugins available. Install some plugins first.';
'কোন প্লাগইন উপলব্ধ নেই। প্রথমে কিছু প্লাগইন ইনস্টল করুন।';

@override
String get widgetUpdateNote =>
'উইজেট ব্যাকগ্রাউন্ডে প্রতি ১৫ মিনিটে আপডেট হয়। তাত্ক্ষণিক আপডেটের জন্য অ্যাপ খুলুন।';
}
14 changes: 9 additions & 5 deletions lib/l10n/app_localizations_de.dart
Original file line number Diff line number Diff line change
Expand Up @@ -505,18 +505,22 @@ class AppLocalizationsDe extends AppLocalizations {
'Crossbar - Universelles Plugin-System\n\nCopyright (C) 2025\n\nDieses Programm ist freie Software: Sie können es unter den Bedingungen der GNU Affero General Public License wie von der Free Software Foundation veröffentlicht, entweder Version 3 der Lizenz oder (nach Ihrer Wahl) jeder späteren Version, weitergeben und/oder modifizieren.\n\nDieses Programm wird in der Hoffnung verteilt, dass es nützlich sein wird, aber OHNE JEDE GARANTIE; sogar ohne die implizite Garantie der MARKTFÄHIGKEIT oder EIGNUNG FÜR EINEN BESTIMMTEN ZWECK. Siehe die GNU Affero General Public License für weitere Details.\n\nSie sollten eine Kopie der GNU Affero General Public License zusammen mit diesem Programm erhalten haben. Falls nicht, siehe <https://www.gnu.org/licenses/>.';

@override
String get widgetConfiguration => 'Widget Configuration';
String get widgetConfiguration => 'Widget-Konfiguration';

@override
String get selectPluginsForWidget => 'Select plugins to display';
String get selectPluginsForWidget => 'Plugins zum Anzeigen auswählen';

@override
String get selectOnePlugin => 'Select one plugin';
String get selectOnePlugin => 'Ein Plugin auswählen';

@override
String get selectPlugins => 'Select plugins';
String get selectPlugins => 'Plugins auswählen';

@override
String get noPluginsAvailable =>
'No plugins available. Install some plugins first.';
'Keine Plugins verfügbar. Installieren Sie zuerst einige Plugins.';

@override
String get widgetUpdateNote =>
'Widgets werden im Hintergrund alle 15 Min. aktualisiert. Öffnen Sie die App für sofortige Updates.';
}
4 changes: 4 additions & 0 deletions lib/l10n/app_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -513,4 +513,8 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get noPluginsAvailable =>
'No plugins available. Install some plugins first.';

@override
String get widgetUpdateNote =>
'Widgets update every 15 min in background. Open the app for instant updates.';
}
Loading