Skip to content

Commit bd13db4

Browse files
committed
fix(tray): refactor menu and update battery plugins
Tray Menu Fixes: - Separate mode now uses tray_manager for crossbar icon + SNI for plugins - Added Disable action to plugin menus (replaces Quit) - Added debug logging for menu creation Sample Plugins Updates: - Battery upgraded from .30s to .2s (faster refresh) - Removed Python/Node/Dart/Go/Rust variants (now sh+lua only) - battery.2s.lua uses crossbarBridge.batterySync for cross-platform Known Issues: - tray_manager setContextMenu may not work on Linux (menu appears empty) - This is a limitation of the tray_manager package on GNOME
1 parent ba49eb5 commit bd13db4

File tree

9 files changed

+153
-290
lines changed

9 files changed

+153
-290
lines changed

lib/services/sample_plugins_service.dart

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,18 +58,13 @@ class SamplePluginsService {
5858
PluginMetadata(
5959
id: 'battery',
6060
name: 'Battery Status',
61-
description: 'Shows battery level and charging status',
61+
description: 'Shows battery level and charging status with dynamic icons',
6262
category: PluginCategory.system,
6363
mobileCompatible: true,
6464
tags: ['battery', 'power', 'charging'],
6565
variants: [
66-
PluginMetadataVariant(language: PluginLanguage.bash, filename: 'battery.30s.sh', assetPath: 'plugins/battery/battery.30s.sh'),
67-
PluginMetadataVariant(language: PluginLanguage.python, filename: 'battery.30s.py', assetPath: 'plugins/battery/battery.30s.py'),
68-
PluginMetadataVariant(language: PluginLanguage.node, filename: 'battery.30s.js', assetPath: 'plugins/battery/battery.30s.js'),
69-
PluginMetadataVariant(language: PluginLanguage.dart, filename: 'battery.30s.dart', assetPath: 'plugins/battery/battery.30s.dart'),
70-
PluginMetadataVariant(language: PluginLanguage.go, filename: 'battery.30s.go', assetPath: 'plugins/battery/battery.30s.go'),
71-
PluginMetadataVariant(language: PluginLanguage.rust, filename: 'battery.30s.rs', assetPath: 'plugins/battery/battery.30s.rs'),
72-
PluginMetadataVariant(language: PluginLanguage.lua, filename: 'battery.30s.lua', assetPath: 'plugins/battery/battery.30s.lua'),
66+
PluginMetadataVariant(language: PluginLanguage.bash, filename: 'battery.2s.sh', assetPath: 'plugins/battery/battery.2s.sh'),
67+
PluginMetadataVariant(language: PluginLanguage.lua, filename: 'battery.2s.lua', assetPath: 'plugins/battery/battery.2s.lua'),
7368
],
7469
),
7570
PluginMetadata(

lib/services/tray_service.dart

Lines changed: 80 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:tray_manager/tray_manager.dart';
99
import '../core/plugin_manager.dart';
1010
import 'package:crossbar_core/crossbar_core.dart' as plugin_model;
1111
import 'logger_service.dart';
12+
import 'refresh_service.dart';
1213
import 'scheduler_service.dart';
1314
import 'settings_service.dart';
1415
import 'tray/tray_backend.dart';
@@ -76,11 +77,13 @@ class TrayService with TrayListener {
7677

7778
// Set up tray based on mode
7879
if (_useSeparateMode) {
79-
// SEPARATE MODE: Use only SNI backend, no unified tray_manager
80-
LoggerService().info('TrayService: Using separate mode (SNI)');
81-
// Icons will be created when plugins output is received
80+
// SEPARATE MODE: Use BOTH tray_manager (crossbar control) + SNI daemons (plugins)
81+
LoggerService().info('TrayService: Using separate mode (mixed)');
82+
// Initialize tray_manager for crossbar control icon (Show/Quit)
83+
await _initUnifiedTray();
84+
// Plugin icons will be created via daemons when plugins output is received
8285
} else {
83-
// UNIFIED MODE: Use tray_manager for single icon
86+
// UNIFIED MODE: Use tray_manager for single icon with all plugins
8487
LoggerService().info('TrayService: Using unified mode (tray_manager)');
8588
await _initUnifiedTray();
8689
}
@@ -208,39 +211,53 @@ class TrayService with TrayListener {
208211

209212
final menuItems = <MenuItem>[];
210213

211-
// Plugin outputs
212-
for (final plugin in _pluginManager.plugins.where((p) => p.enabled)) {
213-
final output = _pluginOutputs[plugin.id];
214-
if (output != null && output.text != null && output.text!.isNotEmpty) {
215-
if (output.menu.isNotEmpty) {
216-
final submenuItems = _convertMenuItems(output.menu);
217-
menuItems.add(MenuItem.submenu(
218-
label: '${output.icon} ${output.text}',
219-
submenu: Menu(items: submenuItems),
220-
));
221-
} else {
222-
menuItems.add(MenuItem(
223-
label: '${output.icon} ${output.text}',
224-
disabled: true,
225-
));
214+
// Plugin outputs - only show in unified mode (in separate mode they have their own icons)
215+
if (!_useSeparateMode) {
216+
for (final plugin in _pluginManager.plugins.where((p) => p.enabled)) {
217+
final output = _pluginOutputs[plugin.id];
218+
if (output != null && output.text != null && output.text!.isNotEmpty) {
219+
if (output.menu.isNotEmpty) {
220+
final submenuItems = _convertMenuItems(output.menu);
221+
menuItems.add(MenuItem.submenu(
222+
label: '${output.icon} ${output.text}',
223+
submenu: Menu(items: submenuItems),
224+
));
225+
} else {
226+
menuItems.add(MenuItem(
227+
label: '${output.icon} ${output.text}',
228+
disabled: true,
229+
));
230+
}
226231
}
227232
}
228-
}
229233

230-
if (menuItems.isNotEmpty) {
231-
menuItems.add(MenuItem.separator());
234+
if (menuItems.isNotEmpty) {
235+
menuItems.add(MenuItem.separator());
236+
}
232237
}
233238

234239
// Standard menu items
235-
menuItems.addAll([
236-
MenuItem(key: 'show', label: 'Show Crossbar'),
237-
MenuItem(key: 'refresh', label: 'Refresh All Plugins'),
238-
MenuItem.separator(),
239-
MenuItem(key: 'quit', label: 'Quit'),
240-
]);
240+
if (_useSeparateMode) {
241+
// In separate mode, only Show and Quit (no Refresh All since plugins have their own)
242+
menuItems.addAll([
243+
MenuItem(key: 'show', label: 'Show'),
244+
MenuItem.separator(),
245+
MenuItem(key: 'quit', label: 'Quit'),
246+
]);
247+
} else {
248+
menuItems.addAll([
249+
MenuItem(key: 'show', label: 'Show Crossbar'),
250+
MenuItem(key: 'refresh', label: 'Refresh All Plugins'),
251+
MenuItem.separator(),
252+
MenuItem(key: 'quit', label: 'Quit'),
253+
]);
254+
}
255+
256+
LoggerService().info('TrayService: Setting menu with ${menuItems.length} items, separateMode=$_useSeparateMode');
241257

242258
try {
243259
await trayManager.setContextMenu(Menu(items: menuItems));
260+
LoggerService().info('TrayService: Menu set successfully');
244261
} catch (e) {
245262
LoggerService().warning('Failed to set tray context menu: $e');
246263
}
@@ -370,9 +387,9 @@ class TrayService with TrayListener {
370387
// Standard actions
371388
items.addAll([
372389
TrayMenuItem(label: 'Refresh', key: 'refresh_$pluginId'),
390+
TrayMenuItem(label: 'Disable', key: 'disable_$pluginId'),
373391
const TrayMenuItem.separator(),
374392
const TrayMenuItem(label: 'Show Crossbar', key: 'show'),
375-
const TrayMenuItem(label: 'Quit', key: 'quit'),
376393
]);
377394

378395
return items;
@@ -487,7 +504,23 @@ class TrayService with TrayListener {
487504

488505
@override
489506
void onTrayMenuItemClick(MenuItem menuItem) {
490-
switch (menuItem.key) {
507+
final key = menuItem.key ?? '';
508+
509+
// Handle disable_pluginId actions
510+
if (key.startsWith('disable_')) {
511+
final pluginId = key.substring(8);
512+
_disablePlugin(pluginId);
513+
return;
514+
}
515+
516+
// Handle refresh_pluginId actions
517+
if (key.startsWith('refresh_')) {
518+
final pluginId = key.substring(8);
519+
SchedulerService().runPluginNow(pluginId);
520+
return;
521+
}
522+
523+
switch (key) {
491524
case 'show':
492525
WindowService().show();
493526
case 'refresh':
@@ -497,6 +530,23 @@ class TrayService with TrayListener {
497530
}
498531
}
499532

533+
/// Disables a plugin and removes its tray icon.
534+
Future<void> _disablePlugin(String pluginId) async {
535+
// Disable the plugin via RefreshService (handles outputs and notifications)
536+
await RefreshService().disablePlugin(pluginId);
537+
538+
// Remove the tray icon
539+
final iconId = _pluginIconIds.remove(pluginId);
540+
if (iconId != null && _backend != null) {
541+
await _backend!.destroyIcon(iconId);
542+
}
543+
544+
// Clear output
545+
_pluginOutputs.remove(pluginId);
546+
547+
LoggerService().info('TrayService: Disabled plugin $pluginId');
548+
}
549+
500550
Future<void> dispose() async {
501551
if (!_initialized) return;
502552

plugins/battery/battery.2s.lua

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
-- battery.2s.lua - Battery monitor with dynamic icons (argos-compatible)
2+
-- Updates every 2 seconds for real-time monitoring
3+
--
4+
-- Features:
5+
-- - Uses Crossbar API (crossbarBridge.batterySync)
6+
-- - Dynamic Freedesktop icons (battery-level-X-symbolic)
7+
-- - Works on ALL platforms (Lua runs embedded)
8+
9+
local bridge = crossbarBridge
10+
11+
-- Get battery data
12+
local battery = bridge.batterySync()
13+
14+
if type(battery) ~= "table" then
15+
print("🔋 --")
16+
print("---")
17+
print("No battery detected")
18+
return
19+
end
20+
21+
local level = battery.level or 0
22+
local charging = battery.charging or false
23+
local state = charging and "charging" or "discharging"
24+
25+
-- Determine icon based on level and state
26+
local function get_battery_icon(pct, st)
27+
local l = math.floor(pct / 10) * 10
28+
if l > 100 then l = 100 end
29+
if l < 0 then l = 0 end
30+
31+
local suffix = ""
32+
if st == "charging" then
33+
suffix = "-charging"
34+
elseif pct == 100 and st == "charging" then
35+
suffix = "-charged"
36+
end
37+
38+
if pct == 100 then
39+
return "battery-level-100-charged-symbolic"
40+
elseif pct <= 5 then
41+
return "battery-level-0" .. suffix .. "-symbolic"
42+
else
43+
return "battery-level-" .. l .. suffix .. "-symbolic"
44+
end
45+
end
46+
47+
local icon_name = get_battery_icon(level, state)
48+
49+
-- Determine color
50+
local color
51+
if state == "discharging" then
52+
if level < 20 then
53+
color = "#ff5555"
54+
else
55+
color = "#f8f8f2"
56+
end
57+
else
58+
color = "#8be9fd"
59+
end
60+
61+
-- Emoji icon
62+
local emoji = charging and "" or (level <= 20 and "🪫" or "🔋")
63+
64+
-- Output
65+
print(emoji .. " " .. level .. "% | iconName=" .. icon_name .. " color=" .. color)
66+
print("---")
67+
print("State: " .. state)
68+
print("Level: " .. level .. "%")
69+
print("---")
70+
print("Refresh | refresh=true")

plugins/battery/battery.30s.dart

Lines changed: 0 additions & 40 deletions
This file was deleted.

plugins/battery/battery.30s.go

Lines changed: 0 additions & 51 deletions
This file was deleted.

plugins/battery/battery.30s.js

Lines changed: 0 additions & 36 deletions
This file was deleted.

0 commit comments

Comments
 (0)