From dda6bfff4280fecbbec2a98f47da9ea8c8d77465 Mon Sep 17 00:00:00 2001 From: Nate Anderson Date: Tue, 17 Jun 2025 20:08:39 -0600 Subject: [PATCH] Added xp-overlay feature --- flake.nix | 4 +- xp_server/bin/xp_nix.dart | 38 +++- xp_server/lib/src/config/config_manager.dart | 32 ++++ .../src/monitors/productivity_monitor.dart | 16 +- .../src/overlays/cursor_overlay_manager.dart | 170 ++++++++++++++++-- .../lib/src/web/static/flutter_bootstrap.js | 2 +- .../src/web/static/flutter_service_worker.js | 2 +- xp_server/pubspec.lock | 2 +- xp_server/pubspec.yaml | 2 +- 9 files changed, 246 insertions(+), 22 deletions(-) diff --git a/flake.nix b/flake.nix index d301097..40fb1d0 100644 --- a/flake.nix +++ b/flake.nix @@ -28,7 +28,9 @@ gtk3 pcre libepoxy - # For drift + glib.dev + sysprof + # For drift / sqlite3 dart sqlite # For xp overlay gtk-layer-shell diff --git a/xp_server/bin/xp_nix.dart b/xp_server/bin/xp_nix.dart index fbc0d3b..79cc1ab 100644 --- a/xp_server/bin/xp_nix.dart +++ b/xp_server/bin/xp_nix.dart @@ -185,8 +185,15 @@ Requirements: break; case 'test': if (parts.length > 1) { - final level = int.tryParse(parts[1]) ?? 1; - _monitor.testTheme(level); + final levelOrCommand = parts[1].toLowerCase(); + if (levelOrCommand == 'overlay') { + _testOverlay(parts.length > 2 ? int.tryParse(parts[2]) : null); + } else { + final level = int.tryParse(levelOrCommand) ?? 1; + _monitor.testTheme(level); + } + } else { + print('Usage: test [level|overlay] - Specify a level number or "overlay" to test XP overlay'); } break; case 'restore': @@ -203,6 +210,7 @@ Requirements: Available commands: - stats: Show current productivity stats - test [level]: Test theme for specific level +- test overlay [value]: Test XP overlay at cursor position - restore: Restore desktop backup - refresh: Refresh base config from current system config - build: Build Flutter dashboard and copy to static files @@ -246,6 +254,32 @@ void _handleBuildCommand() async { } } +/// Test the XP overlay with an optional XP value +Future _testOverlay(int? xpValue) async { + print('๐Ÿงช Testing XP overlay system...'); + final windowManager = WindowManagerDetector.instance.detect(); + + if (windowManager == WindowManager.unknown) { + print('โŒ No supported window manager detected'); + return; + } + + final overlayManager = _monitor.cursorOverlayManager; + if (overlayManager == null) { + print('โŒ Overlay manager not initialized'); + return; + } + + final available = await overlayManager.isAvailable(); + if (!available) { + print('โŒ Overlay system is not available. Check logs for details.'); + return; + } + + print('โœ… Overlay system is available, showing test overlay...'); + await overlayManager.testOverlay(testXP: xpValue ?? 42); +} + Future _quit() async { _monitor.stop(); await _dashboardServer.stop(); diff --git a/xp_server/lib/src/config/config_manager.dart b/xp_server/lib/src/config/config_manager.dart index d368c9d..c28194a 100644 --- a/xp_server/lib/src/config/config_manager.dart +++ b/xp_server/lib/src/config/config_manager.dart @@ -92,6 +92,16 @@ class ConfigManager { }, "zoom_multipliers": {"active_meeting": 8, "background_meeting": 5, "zoom_focused": 2, "zoom_background": 0}, }, + "overlay": { + "enabled": true, + "app_path": "", // Auto-detect if empty + "duration_seconds": 3, + "colors": { + "regular": "#4CAF50", // Green + "high": "#FF9800", // Orange + "bonus": "#FFC107", // Amber + }, + }, "achievements": { "level_based": { "5": { @@ -199,6 +209,15 @@ class ConfigManager { _ensureInitialized(); return _config!['logging'] ?? {}; } + + Map get overlay { + _ensureInitialized(); + return _config!['overlay'] ?? { + "enabled": true, + "app_path": "", + "duration_seconds": 3, + }; + } // Specific getters for commonly used values int getBaseXP(String activityType) { @@ -270,6 +289,19 @@ class ConfigManager { int calculateLevel(int totalXP) { return (totalXP / getXPPerLevel()).floor() + 1; } + + // Overlay configuration getters + String? getOverlayAppPath() { + return overlay['app_path'] as String?; + } + + bool isOverlayEnabled() { + return overlay['enabled'] as bool? ?? true; + } + + int getOverlayDuration() { + return overlay['duration_seconds'] as int? ?? 3; + } // Update configuration programmatically Future updateConfig(String path, dynamic value) async { diff --git a/xp_server/lib/src/monitors/productivity_monitor.dart b/xp_server/lib/src/monitors/productivity_monitor.dart index 5751618..f1a3e35 100644 --- a/xp_server/lib/src/monitors/productivity_monitor.dart +++ b/xp_server/lib/src/monitors/productivity_monitor.dart @@ -66,13 +66,22 @@ class ProductivityMonitor { // Initialize cursor overlay manager if (_windowManager != null) { - final overlayAppPath = '/home/nate/source/xp_nix/xp_overlay_app/build/linux/x64/release/bundle/xp_overlay_app'; _cursorOverlayManager = CursorOverlayManager( windowManager: _windowManager!, - overlayAppPath: overlayAppPath, + enabled: ConfigManager.instance.isOverlayEnabled(), ); + + // Log overlay availability on startup + _cursorOverlayManager!.isAvailable().then((available) { + if (available) { + Logger.info('XP cursor overlay system initialized successfully'); + } else { + Logger.warn('XP cursor overlay system is not available'); + } + }); } else { _cursorOverlayManager = null; + Logger.warn('Window manager not available, cursor overlay disabled'); } // Initialize zoom detector (only if not in test mode) @@ -739,6 +748,9 @@ class ProductivityMonitor { Future checkForLevelUpNow() async { await _checkForLevelUp(); } + + /// Get the cursor overlay manager (used for testing) + CursorOverlayManager? get cursorOverlayManager => _cursorOverlayManager; // WebSocket broadcasting methods void _broadcastLevelUp(int newLevel) { diff --git a/xp_server/lib/src/overlays/cursor_overlay_manager.dart b/xp_server/lib/src/overlays/cursor_overlay_manager.dart index f469626..b3165bd 100644 --- a/xp_server/lib/src/overlays/cursor_overlay_manager.dart +++ b/xp_server/lib/src/overlays/cursor_overlay_manager.dart @@ -1,24 +1,83 @@ import 'dart:io'; +import 'dart:convert'; // For utf8 +import 'package:path/path.dart' as p; import '../interfaces/i_window_manager.dart'; import '../logging/logger.dart'; +import '../config/config_manager.dart'; /// Manages cursor-positioned XP overlays by coordinating cursor tracking /// and spawning Flutter overlay processes class CursorOverlayManager { final IWindowManager _windowManager; final String _overlayAppPath; + final bool _enabled; + /// Create a CursorOverlayManager with the specified window manager and overlay app path + /// + /// If overlayAppPath is not provided, tries to resolve the path automatically CursorOverlayManager({ required IWindowManager windowManager, - required String overlayAppPath, + String? overlayAppPath, + bool enabled = true, }) : _windowManager = windowManager, - _overlayAppPath = overlayAppPath; + _overlayAppPath = overlayAppPath ?? _resolveOverlayAppPath(), + _enabled = enabled; + + /// Attempts to find the overlay app binary path + static String _resolveOverlayAppPath() { + final List pathOptions = [ + // Check for configuration in xp_config.json + ConfigManager.instance.getOverlayAppPath(), + ]; + + final List possiblePaths = [ + // Check relative to the current script directory + p.join(p.dirname(Platform.script.toFilePath()), '..', '..', '..', 'xp_overlay_app', 'build', 'linux', 'x64', 'release', 'bundle', 'xp_overlay_app'), + + // Check relative to the current working directory + p.join(Directory.current.path, '..', 'xp_overlay_app', 'build', 'linux', 'x64', 'release', 'bundle', 'xp_overlay_app'), + + // Fixed paths for development + '/home/nate/source/non-work/xp_nix/xp_overlay_app/build/linux/x64/release/bundle/xp_overlay_app', + ]; + + // Add non-null paths from options to possible paths + for (final path in pathOptions) { + if (path != null && path.isNotEmpty) { + possiblePaths.add(path); + } + } + + for (final path in possiblePaths) { + if (path != null && path.isNotEmpty && File(path).existsSync()) { + Logger.info('Found overlay app at: $path'); + return path; + } + } + + Logger.warn('Could not find overlay app binary, overlays will be disabled'); + return ''; + } /// Show XP number at current cursor position /// /// [xpValue] - XP amount to display - /// [duration] - How long to show the overlay (default 3 seconds) - Future showXPNumber(int xpValue, {Duration duration = const Duration(seconds: 3)}) async { + /// [duration] - How long to show the overlay (default from config, fallback 3 seconds) + Future showXPNumber( + int xpValue, { + Duration? duration, + }) async { + // Skip if disabled + if (!_enabled || !ConfigManager.instance.isOverlayEnabled()) { + return; + } + + // Skip if overlay app path is empty or doesn't exist + if (_overlayAppPath.isEmpty || !await File(_overlayAppPath).exists()) { + Logger.debug('Overlay app not found at: $_overlayAppPath, skipping XP overlay'); + return; + } + try { // Get current cursor position final cursorPos = await _windowManager.getCursorPosition(); @@ -30,10 +89,14 @@ class CursorOverlayManager { final x = cursorPos['x']!; final y = cursorPos['y']!; + // Use specified duration or get from config + final overlayDuration = duration ?? + Duration(seconds: ConfigManager.instance.getOverlayDuration()); + Logger.info('Showing XP overlay: +$xpValue at position ($x, $y)'); // Spawn Flutter overlay process - await _spawnOverlayProcess(x, y, xpValue, duration); + await _spawnOverlayProcess(x, y, xpValue, overlayDuration); } catch (e) { Logger.error('Failed to show XP overlay', e); } @@ -44,11 +107,31 @@ class CursorOverlayManager { /// [x] - X coordinate in pixels /// [y] - Y coordinate in pixels /// [xpValue] - XP amount to display - /// [duration] - How long to show the overlay - Future showXPNumberAt(int x, int y, int xpValue, {Duration duration = const Duration(seconds: 3)}) async { + /// [duration] - How long to show the overlay (default from config) + Future showXPNumberAt( + int x, + int y, + int xpValue, + {Duration? duration} + ) async { + // Skip if disabled + if (!_enabled || !ConfigManager.instance.isOverlayEnabled()) { + return; + } + + // Skip if overlay app path is empty or doesn't exist + if (_overlayAppPath.isEmpty || !await File(_overlayAppPath).exists()) { + Logger.debug('Overlay app not found at: $_overlayAppPath, skipping XP overlay'); + return; + } + try { + // Use specified duration or get from config + final overlayDuration = duration ?? + Duration(seconds: ConfigManager.instance.getOverlayDuration()); + Logger.info('Showing XP overlay: +$xpValue at position ($x, $y)'); - await _spawnOverlayProcess(x, y, xpValue, duration); + await _spawnOverlayProcess(x, y, xpValue, overlayDuration); } catch (e) { Logger.error('Failed to show XP overlay at position', e); } @@ -70,6 +153,12 @@ class CursorOverlayManager { Future _spawnOverlayProcess(int x, int y, int xpValue, Duration duration) async { final durationSeconds = duration.inSeconds; + // Safety check - don't proceed if path is empty + if (_overlayAppPath.isEmpty) { + Logger.warn('Overlay app path is empty, cannot spawn process'); + return; + } + // Check if overlay app exists if (!await File(_overlayAppPath).exists()) { Logger.error('Overlay app not found at: $_overlayAppPath'); @@ -77,15 +166,26 @@ class CursorOverlayManager { } try { + // Log the command we're about to run + final commandArgs = [x.toString(), y.toString(), xpValue.toString(), durationSeconds.toString()]; + Logger.debug('Executing: $_overlayAppPath ${commandArgs.join(' ')}'); + // Spawn overlay process with arguments: x y xp_value duration_seconds final process = await Process.start( _overlayAppPath, - [x.toString(), y.toString(), xpValue.toString(), durationSeconds.toString()], + commandArgs, mode: ProcessStartMode.detached, ); Logger.debug('Spawned overlay process with PID: ${process.pid}'); + // Collect stderr output to log if there's an error + process.stderr.transform(utf8.decoder).listen((data) { + if (data.isNotEmpty) { + Logger.warn('Overlay process stderr: $data'); + } + }); + // Don't wait for the process to complete - let it run independently process.exitCode.then((exitCode) { if (exitCode != 0) { @@ -98,19 +198,59 @@ class CursorOverlayManager { }); } catch (e) { - Logger.error('Failed to spawn overlay process', e); + Logger.error('Failed to spawn overlay process: $e'); } } /// Test the overlay system by showing a test XP number + /// + /// Performs additional validation and prints detailed diagnostic information Future testOverlay({int testXP = 42}) async { Logger.info('Testing overlay system with XP value: $testXP'); - await showXPNumber(testXP, duration: const Duration(seconds: 5)); + + // Check if overlay is enabled in config + final enabled = ConfigManager.instance.isOverlayEnabled(); + Logger.info('Overlay enabled in config: $enabled'); + + // Check if overlay app exists + final appExists = _overlayAppPath.isNotEmpty && await File(_overlayAppPath).exists(); + Logger.info('Overlay app path: $_overlayAppPath (exists: $appExists)'); + + // Test cursor position detection + final cursorPos = await getCurrentCursorPosition(); + if (cursorPos != null) { + Logger.info('Cursor position detected at: (${cursorPos['x']}, ${cursorPos['y']})'); + } else { + Logger.warn('Could not detect cursor position'); + } + + // Check window manager type + Logger.info('Window manager type: ${_windowManager.managerType}'); + + // Show test overlay if everything is ready + if (enabled && appExists && cursorPos != null) { + await showXPNumber(testXP, duration: const Duration(seconds: 5)); + Logger.info('Test overlay triggered successfully'); + } else { + Logger.warn('Test overlay skipped due to validation failures'); + } } - /// Check if the overlay system is available + /// Check if the overlay system is available and ready to use Future isAvailable() async { try { + // Check if overlay is enabled in config + if (!_enabled || !ConfigManager.instance.isOverlayEnabled()) { + Logger.debug('Overlay system disabled by configuration'); + return false; + } + + // Check if overlay app path is valid + if (_overlayAppPath.isEmpty) { + Logger.debug('Overlay app path is empty'); + return false; + } + // Check if window manager supports cursor position final cursorPos = await _windowManager.getCursorPosition(); final windowManagerAvailable = cursorPos != null; @@ -118,7 +258,11 @@ class CursorOverlayManager { // Check if overlay app exists final overlayAppAvailable = await File(_overlayAppPath).exists(); - Logger.debug('Overlay system availability: windowManager=$windowManagerAvailable, overlayApp=$overlayAppAvailable'); + Logger.debug('Overlay system availability check: ' + + 'enabled=$_enabled, ' + + 'windowManager=$windowManagerAvailable, ' + + 'overlayAppExists=$overlayAppAvailable, ' + + 'overlayAppPath=$_overlayAppPath'); return windowManagerAvailable && overlayAppAvailable; } catch (e) { diff --git a/xp_server/lib/src/web/static/flutter_bootstrap.js b/xp_server/lib/src/web/static/flutter_bootstrap.js index 1512737..2d2bae2 100644 --- a/xp_server/lib/src/web/static/flutter_bootstrap.js +++ b/xp_server/lib/src/web/static/flutter_bootstrap.js @@ -39,6 +39,6 @@ _flutter.buildConfig = {"engineRevision":"18818009497c581ede5d8a3b8b833b81d00ceb _flutter.loader.load({ serviceWorkerSettings: { - serviceWorkerVersion: "2823181200" + serviceWorkerVersion: "3663276084" } }); diff --git a/xp_server/lib/src/web/static/flutter_service_worker.js b/xp_server/lib/src/web/static/flutter_service_worker.js index f8fd659..5aab3b0 100644 --- a/xp_server/lib/src/web/static/flutter_service_worker.js +++ b/xp_server/lib/src/web/static/flutter_service_worker.js @@ -22,7 +22,7 @@ const RESOURCES = {"assets/AssetManifest.bin": "693635b5258fe5f1cda720cf224f158c "icons/Icon-512.png": "96e752610906ba2a93c65f8abe1645f1", "icons/Icon-192.png": "ac9a721a12bbc803b44f645561ecb1e1", "main.dart.js": "7f6b340356b31753eb9d3f857246a6b6", -"flutter_bootstrap.js": "5f2f5895eadd961a24b30c35e0e0b956", +"flutter_bootstrap.js": "eb3490973c14b6084c78d6130df851d9", "canvaskit/canvaskit.js.symbols": "27361387bc24144b46a745f1afe92b50", "canvaskit/skwasm.wasm": "1c93738510f202d9ff44d36a4760126b", "canvaskit/canvaskit.js": "728b2d477d9b8c14593d4f9b82b484f3", diff --git a/xp_server/pubspec.lock b/xp_server/pubspec.lock index 03dbbe9..1c3c354 100644 --- a/xp_server/pubspec.lock +++ b/xp_server/pubspec.lock @@ -210,7 +210,7 @@ packages: source: hosted version: "2.2.0" path: - dependency: transitive + dependency: "direct main" description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" diff --git a/xp_server/pubspec.yaml b/xp_server/pubspec.yaml index a791552..2bfe78a 100644 --- a/xp_server/pubspec.yaml +++ b/xp_server/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: shelf_static: ^1.1.2 shelf_web_socket: ^2.0.0 web_socket_channel: ^2.4.0 - # path: ^1.8.0 + path: ^1.8.0 dev_dependencies: lints: ^5.0.0