Skip to content

[Feature] Implement Hierarchical Submenu Support for BitBar/Argos Compatibility (v1.5.0) #37

@insign

Description

@insign

💡 Suggestion

This is a suggestion to enhance the tray menu system. The primary objective is to achieve full BitBar/Argos compatibility by implementing hierarchical submenus, allowing plugins to create multi-level menu structures using the standard -- prefix notation.

🎯 Objective

Implement complete hierarchical submenu parsing and rendering for the system tray, following the BitBar specification and Argos conventions. This will enable plugins to create complex nested menus like:

#!/bin/bash
# Example: cpu.10s.sh
echo "🖥️ CPU: 85%"
echo "---"
echo "Status"
echo "--Core 1: 90% | color=red"
echo "--Core 2: 80% | color=orange"
echo "----Details"
echo "------Frequency: 3.2 GHz"
echo "------Temperature: 65°C"
echo "Settings"
echo "--Refresh Rate"
echo "----5 seconds | bash='crossbar config set refresh 5s'"
echo "----10 seconds | bash='crossbar config set refresh 10s'"

Expected Result: A tray menu with proper indentation/hierarchy:

🖥️ CPU: 85%
---
Status
  ├─ Core 1: 90% (red text)
  ├─ Core 2: 80% (orange text)
  └─ Details
       ├─ Frequency: 3.2 GHz
       └─ Temperature: 65°C
Settings
  └─ Refresh Rate
       ├─ 5 seconds
       └─ 10 seconds

📋 Current State Analysis

What Already Works ✅

  • Basic menu parsing in lib/core/output_parser.dart (_parseBitBar method)
  • MenuItem model already has submenu field (lib/models/plugin_output.dart)
  • JSON format supports hierarchical menus:
    {
      "menu": [
        {"text": "Parent", "submenu": [{"text": "Child"}]}
      ]
    }

What's Missing ❌

  • BitBar text format doesn't parse -- prefixes to create hierarchy
  • Current parser treats all menu items as flat list (level 0)
  • Tray service needs recursive menu building logic

🛠️ Implementation Suggestions

1. Enhance BitBar Parser

Location: lib/core/output_parser.dart (around line 70, in _parseBitBar method)

Current behavior: All lines after --- are treated as top-level menu items.

Suggested approach:

// Parse hierarchy level based on leading dashes
static int _getMenuLevel(String line) {
  int level = 0;
  for (int i = 0; i < line.length; i++) {
    if (line[i] == '-' && (i + 1 < line.length && line[i + 1] == '-')) {
      level++;
      i++; // Skip next dash
    } else {
      break;
    }
  }
  return level;
}

// Build hierarchical structure from flat list
static List<MenuItem> _buildMenuHierarchy(List<_MenuItemFlat> flatItems) {
  final List<MenuItem> rootItems = [];
  final Map<int, MenuItem> lastAtLevel = {};
  
  for (final flat in flatItems) {
    final item = MenuItem(
      text: flat.text,
      bash: flat.bash,
      href: flat.href,
      color: flat.color,
    );
    
    if (flat.level == 0) {
      rootItems.add(item);
      lastAtLevel[0] = item;
    } else {
      // Find parent at level-1 and add as submenu
      final parent = lastAtLevel[flat.level - 1];
      if (parent != null) {
        parent.submenu ??= [];
        parent.submenu!.add(item);
      }
      lastAtLevel[flat.level] = item;
    }
  }
  
  return rootItems;
}

Reference: BitBar spec defines levels as:

  • Text = level 0 (root menu item)
  • --Text = level 1 (submenu of previous level 0)
  • ----Text = level 2 (submenu of previous level 1)
  • ------Text = level 3, and so on...

2. Update MenuItem Model (if needed)

Location: lib/models/plugin_output.dart

Current state: MenuItem already has optional submenu field, which is good!

Optional enhancement: Add level field for debugging/validation:

class MenuItem {
  final int level; // For debugging - not strictly necessary
  // ... existing fields
}

3. Implement Recursive Tray Rendering

Location: Create or update tray menu builder (likely in lib/services/tray_service.dart)

Suggested structure:

import 'package:tray_manager/tray_manager.dart';

class TrayMenuBuilder {
  Menu buildMenu(List<MenuItem> items) {
    final menu = Menu();
    _addItemsToMenu(menu, items);
    return menu;
  }
  
  void _addItemsToMenu(Menu menu, List<MenuItem> items) {
    for (final item in items) {
      if (item.separator) {
        menu.addSeparator();
      } else if (item.submenu != null && item.submenu!.isNotEmpty) {
        // Create submenu recursively
        final submenu = Menu();
        _addItemsToMenu(submenu, item.submenu!);
        
        menu.addItem(MenuItem(
          label: item.text ?? '',
          submenu: submenu,
        ));
      } else {
        // Leaf item with action
        menu.addItem(MenuItem(
          label: item.text ?? '',
          onClick: () => _handleClick(item),
        ));
      }
    }
  }
  
  void _handleClick(MenuItem item) {
    if (item.bash != null) {
      // Execute bash command
    } else if (item.href != null) {
      // Open URL
    }
  }
}

Note: The exact API depends on tray_manager package capabilities. Check tray_manager documentation for submenu support.

🧪 Testing Strategy

Unit Tests

  1. Parser tests (test/unit/core/output_parser_test.dart):

    • Single level menu (current behavior)
    • Two-level hierarchy (--)
    • Three-level hierarchy (----)
    • Mixed levels with separators
    • Edge cases: orphaned submenus, invalid indentation
  2. Example test case:

test('parses multi-level BitBar format', () {
  const input = '''
🖥️ CPU: 85%
---
Status
--Core 1: 90%
----Details
------Temperature: 65°C
  ''';
  
  final output = OutputParser.parse(input, 'cpu.10s.sh');
  
  expect(output.menu.length, 1); // "Status"
  expect(output.menu[0].submenu?.length, 1); // "Core 1: 90%"
  expect(output.menu[0].submenu![0].submenu?.length, 1); // "Details"
  expect(output.menu[0].submenu![0].submenu![0].submenu?.length, 1); // "Temperature"
});

Integration Tests

  1. Create test plugins with complex hierarchies
  2. Verify visual rendering in tray (manual QA on Linux/macOS/Windows)

📚 References

✅ Acceptance Criteria

  • BitBar text format with --, ----, etc. creates proper hierarchy
  • Existing JSON format continues to work (backward compatibility)
  • Tray menu renders submenus correctly on all desktop platforms (Linux/macOS/Windows)
  • Unit tests cover 2-5 levels of nesting
  • Integration test with real plugin validates visual output
  • Documentation updated with submenu examples
  • No performance regression (benchmark with 50+ menu items)

🎯 Success Metrics

  • Compatibility: All BitBar/Argos example plugins work without modification
  • Performance: Menu build time < 50ms for 100 items
  • UX: Users can navigate 3+ levels deep intuitively

📝 Additional Notes

  • This feature is critical for v1.5.0 milestone ("Menu Perfeito")
  • Prioritize correctness over performance initially
  • Consider adding max depth limit (e.g., 10 levels) to prevent abuse
  • Some window managers (GNOME) may have limitations on submenu rendering

Related Issues: (will be added as we create v1.6.0+ issues)

Milestone: v1.5.0
Priority: High
Estimated Effort: 2-3 days

Metadata

Metadata

Assignees

No one assigned

    Labels

    julesTriggers Jules for duty

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions