Added sway support and created initial xp overlay with gtk-layer-shell
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"dart",
|
||||
"flutter"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
}
|
||||
+248
-46
@@ -2,75 +2,277 @@
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
XP Nix is a productivity tracking and gamification system designed for Linux desktop environments, supporting both
|
||||
Hyprland and Sway window managers. It transforms daily computer usage into a gamified experience by monitoring
|
||||
activity patterns and providing real-time feedback through an XP (experience points) system.
|
||||
|
||||
### Window Manager Support
|
||||
|
||||
**Hyprland**: Full feature support including visual desktop enhancements, idle monitoring, and activity detection
|
||||
**Sway**: Core functionality with idle monitoring and activity detection (visual enhancements disabled by design)
|
||||
**Detection**: Automatic runtime detection via environment variables (`HYPRLAND_INSTANCE_SIGNATURE`, `SWAYSOCK`)
|
||||
|
||||
## Commands
|
||||
|
||||
### Development
|
||||
- `dart run bin/xp_nix.dart` - Run the main application
|
||||
- `dart test` - Run all tests
|
||||
- `dart run bin/xp_nix.dart` - Run the main application server
|
||||
- `dart run bin/xp_nix.dart --help` - Show command line options
|
||||
- `dart run bin/xp_nix.dart --db custom.db --port 8081` - Run with custom database and port
|
||||
- `dart test` - Run all tests
|
||||
- `dart test test/specific_test.dart` - Run a specific test file
|
||||
- `dart analyze` - Run static analysis
|
||||
- `dart analyze` - Run static analysis and linting
|
||||
- `dart pub get` - Install dependencies
|
||||
- `nix develop` - Enter development environment with Dart and SQLite
|
||||
|
||||
### Interactive Commands (while running)
|
||||
Once the server is running, these commands are available in the terminal:
|
||||
- `stats` - Show current productivity stats
|
||||
- `test [level]` - Test theme for specific level
|
||||
- `restore` - Restore desktop backup
|
||||
- `refresh` - Refresh base config from current system config
|
||||
- `build` - Build Flutter dashboard and copy to static files
|
||||
- `help` - Show available commands
|
||||
|
||||
### Nix Environment
|
||||
This project uses Nix flakes for development environment management. The flake provides Dart SDK and SQLite with proper library paths configured.
|
||||
This project uses Nix flakes for development environment management. The flake provides Dart SDK and SQLite with proper library paths configured:
|
||||
- `nix develop` - Enter development environment with Dart and SQLite
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Dependency Injection & Testing
|
||||
The codebase uses dependency injection through interfaces to enable both production and testing modes:
|
||||
|
||||
- **Interfaces** (`lib/src/interfaces/`): Define contracts for core components
|
||||
- `IActivityDetector` - Window/application activity detection
|
||||
- `IIdleMonitor` - User idle state monitoring
|
||||
- `IDesktopEnhancer` - Desktop theme/visual enhancements
|
||||
- `ITimeProvider` - Time operations (for testing time manipulation)
|
||||
|
||||
- **Production Implementations**: Real system integrations (Hyprland, system time)
|
||||
- **Test Mocks** (`lib/src/testing/`): Controllable implementations for testing
|
||||
|
||||
### Core Components
|
||||
|
||||
**ProductivityMonitor** - Central orchestrator that:
|
||||
**ProductivityMonitor** (`lib/src/monitors/productivity_monitor.dart`) - Central orchestrator that:
|
||||
- Coordinates activity detection, idle monitoring, and desktop enhancement
|
||||
- Calculates XP rewards based on activity types and time multipliers
|
||||
- Manages level progression and achievement system
|
||||
- Handles both event-driven (via IActivityDetector) and polling modes
|
||||
- Supports both event-driven (via IActivityDetector) and legacy polling modes
|
||||
- Broadcasts updates via WebSocket for real-time dashboard updates
|
||||
|
||||
**Activity Classification System**:
|
||||
- Uses `ActivityEventType` enum for categorizing activities (coding, meetings, etc.)
|
||||
- Supports user-defined application classifications stored in database
|
||||
- Fallback categorization based on application names and window titles
|
||||
**DashboardServer** (`lib/src/web/dashboard_server.dart`) - Web server providing:
|
||||
- RESTful API endpoints for stats, achievements, activities, and configuration
|
||||
- Real-time WebSocket connections for live updates
|
||||
- Serves Flutter-built dashboard from static files
|
||||
- Configurable port (default: 8080)
|
||||
|
||||
**XP & Gamification**:
|
||||
- Time-based multipliers for deep work hours vs late night penalty
|
||||
- Focus session bonuses with milestone rewards (60min, 120min, 180min)
|
||||
- Level-based visual themes applied via desktop enhancer
|
||||
- Achievement system with level, focus, and session-based rewards
|
||||
|
||||
### Database Schema
|
||||
SQLite database with tables for:
|
||||
- Daily stats (XP, focus time, meeting time, level)
|
||||
**DatabaseManager** (`lib/src/database/database_manager.dart`) - SQLite database operations for:
|
||||
- Daily stats tracking (XP, focus time, meeting time, level)
|
||||
- Activity events with duration and metadata
|
||||
- Application classifications (user-defined)
|
||||
- Achievements and focus sessions
|
||||
- Theme change history and streak tracking
|
||||
|
||||
### Configuration
|
||||
JSON-based configuration (`config/xp_config.json`) defines:
|
||||
- XP multipliers by activity type and time of day
|
||||
- Achievement definitions and rewards
|
||||
- Focus session bonus structure
|
||||
- Monitoring intervals and thresholds
|
||||
### Dependency Injection & Testing
|
||||
|
||||
### Web Dashboard
|
||||
Built-in web server (`lib/src/web/`) provides real-time dashboard at http://localhost:8080 showing stats, recent activities, and progress visualization.
|
||||
The codebase uses dependency injection through interfaces to enable both production and testing modes:
|
||||
|
||||
**Interfaces** (`lib/src/interfaces/`):
|
||||
- `IActivityDetector` - Window/application activity detection
|
||||
- `IIdleMonitor` - User idle state monitoring
|
||||
- `IDesktopEnhancer` - Desktop theme/visual enhancements
|
||||
- `ITimeProvider` - Time operations (for testing time manipulation)
|
||||
- `IWindowManager` - Window manager operations abstraction
|
||||
|
||||
**Hyprland Implementations**:
|
||||
- `HyprlandActivityDetector` - Uses `hyprctl activewindow` for activity detection
|
||||
- `IdleMonitor` - Uses `hypridle` for idle state monitoring
|
||||
- `HyprlandEnhancer` - Full desktop theming and visual enhancements
|
||||
- `HyprlandManager` - Window manager operations via `hyprctl`
|
||||
|
||||
**Sway Implementations**:
|
||||
- `SwayActivityDetector` - Uses `swaymsg -t get_tree` for activity detection
|
||||
- `SwayIdleMonitor` - Uses `swayidle` for idle state monitoring
|
||||
- `SwayEnhancer` - Notifications-only enhancer (no visual changes)
|
||||
- `SwayManager` - Window manager operations via `swaymsg`
|
||||
|
||||
**Factory Classes** (`lib/src/factories/`):
|
||||
- `IdleMonitorFactory` - Creates appropriate idle monitor for detected window manager
|
||||
- `ActivityDetectorFactory` - Creates appropriate activity detector for detected window manager
|
||||
- `DesktopEnhancerFactory` - Creates appropriate enhancer for detected window manager
|
||||
- `WindowManagerFactory` - Creates appropriate window manager interface
|
||||
|
||||
**Test Mocks** (`lib/src/testing/`):
|
||||
- `MockActivityDetector` - Controllable activity simulation
|
||||
- `MockIdleMonitor` - Controllable idle state simulation
|
||||
- `MockDesktopEnhancer` - No-op desktop enhancement for testing
|
||||
- `MockTimeProvider` - Controllable time for testing
|
||||
- `MockWindowManagerDetector` - Test scenarios for different window managers
|
||||
|
||||
### Activity Classification System
|
||||
|
||||
**ActivityEventType** enum categorizes activities:
|
||||
- `coding` - Development work (10 XP/min base)
|
||||
- `focusedBrowsing` - Research and documentation (6 XP/min base)
|
||||
- `collaboration` - Team communication (7 XP/min base)
|
||||
- `meetings` - Video conferences and calls (3 XP/min base)
|
||||
- `misc` - General productivity (2 XP/min base)
|
||||
- `uncategorized` - Unknown activities (1 XP/min base)
|
||||
|
||||
**Classification Logic**:
|
||||
- Uses user-defined application classifications stored in database
|
||||
- Fallback categorization based on application names and window titles
|
||||
- Tracks unclassified applications for manual categorization
|
||||
|
||||
### XP & Gamification System
|
||||
|
||||
**XP Calculation**:
|
||||
- Base XP per minute varies by activity type
|
||||
- Time-based multipliers from configuration:
|
||||
- Deep work hours (1.5x): 09:00-11:00, 14:00-16:00
|
||||
- Late night penalty (0.8x): 22:00-06:00
|
||||
- Focus session bonuses: 60min (+100 XP), 120min (+200 XP), 180min (+500 XP)
|
||||
|
||||
**Level System**:
|
||||
- 100 XP per level progression
|
||||
- Visual desktop themes applied at different levels
|
||||
- Achievement system with level, focus, and session-based rewards
|
||||
|
||||
**Achievements**:
|
||||
- Level-based: Rising Star (L5), Productivity Warrior (L10), Focus Master (L15), Legendary Achiever (L25)
|
||||
- Focus-based: Deep Focus (4h), Focus Titan (8h)
|
||||
- Session-based: Session Master (5+ sessions)
|
||||
- Meeting-based: Communication Pro (3h meetings)
|
||||
|
||||
### Configuration Management
|
||||
|
||||
**ConfigManager** (`lib/src/config/config_manager.dart`):
|
||||
- Loads from `config/xp_config.json`
|
||||
- Manages XP multipliers, achievement definitions, focus bonuses
|
||||
- Supports runtime configuration updates via web API
|
||||
|
||||
**Key Configuration Sections**:
|
||||
- `xp_rewards` - Base multipliers and time-based modifiers
|
||||
- `achievements` - Achievement definitions and rewards
|
||||
- `level_system` - XP per level and max level
|
||||
- `monitoring` - Polling intervals and thresholds
|
||||
- `logging` - Log levels and file management
|
||||
|
||||
### Web Dashboard & API
|
||||
|
||||
**Dashboard Features**:
|
||||
- Real-time stats display with WebSocket updates
|
||||
- Activity timeline and XP breakdown charts
|
||||
- Achievement gallery and progress tracking
|
||||
- Application classification management
|
||||
- Configuration editor
|
||||
- Log viewer with filtering
|
||||
|
||||
**API Endpoints**:
|
||||
- `GET /api/stats` - Current day statistics
|
||||
- `GET /api/stats/history?days=7` - Historical stats
|
||||
- `GET /api/achievements` - All achievements
|
||||
- `GET /api/activities?limit=100` - Recent activities
|
||||
- `GET /api/focus-sessions` - Focus session history
|
||||
- `GET /api/xp-breakdown` - XP breakdown by activity type
|
||||
- `GET /api/classifications` - Application classifications
|
||||
- `POST /api/classifications` - Save application classification
|
||||
- `GET /api/config` - Current configuration
|
||||
- `POST /api/config` - Update configuration
|
||||
|
||||
**WebSocket Events**:
|
||||
- `level_up` - Level progression notifications
|
||||
- `achievement_unlocked` - Achievement notifications
|
||||
- `stats_update` - Real-time stats updates
|
||||
- `xp_breakdown_update` - XP distribution updates
|
||||
- `focus_session_complete` - Focus session completions
|
||||
|
||||
### Flutter Dashboard Build System
|
||||
|
||||
**DashboardBuilder** (`lib/src/build/dashboard_builder.dart`):
|
||||
- Builds Flutter dashboard from `../xp_dashboard` directory
|
||||
- Copies built web files to `lib/src/web/static/`
|
||||
- Accessible via `build` command while server is running
|
||||
- Requires Flutter to be available in system PATH
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
The architecture supports comprehensive testing through:
|
||||
- Interface-based dependency injection
|
||||
- Time manipulation via `ITimeProvider`
|
||||
- Mock implementations that simulate user behavior
|
||||
- Simulation tests that model complete work days
|
||||
- Integration tests for idle detection and activity consolidation
|
||||
### Test Structure
|
||||
- **Unit Tests**: Individual component testing with mocks
|
||||
- **Integration Tests**: WebSocket and database integration
|
||||
- **Simulation Tests**: Complete work day scenarios
|
||||
- **Configuration Tests**: Hyprland config parsing
|
||||
|
||||
### Key Test Files
|
||||
- `test/xp_nix_test.dart` - Main productivity monitor tests
|
||||
- `test/websocket_integration_test.dart` - WebSocket functionality
|
||||
- `test/simulation/` - Work day simulation scenarios
|
||||
- `test/deep_idle_test.dart` - Idle detection edge cases
|
||||
- `test/hyprland_config_parser_test.dart` - Configuration parsing
|
||||
- `test/sway_integration_test.dart` - Sway window manager integration tests
|
||||
|
||||
### Testing Utilities
|
||||
- `test/create_golden_db.dart` - Creates test database with sample data
|
||||
- Time manipulation via `MockTimeProvider` for deterministic testing
|
||||
- Activity simulation via `MockActivityDetector` for scenario testing
|
||||
|
||||
## System Requirements & Compatibility
|
||||
|
||||
### Window Manager Requirements
|
||||
|
||||
**Hyprland Environment**:
|
||||
- `hyprctl` - Window management and querying
|
||||
- `hypridle` - Idle detection and monitoring
|
||||
- Environment: `$HYPRLAND_INSTANCE_SIGNATURE` must be set
|
||||
|
||||
**Sway Environment**:
|
||||
- `swaymsg` - Window management and querying
|
||||
- `swayidle` - Idle detection and monitoring
|
||||
- Environment: `$SWAYSOCK` must be set
|
||||
|
||||
**Optional Dependencies**:
|
||||
- `notify-send` - Desktop notifications (both window managers)
|
||||
- `mako` or `dunst` - Enhanced notification daemons (Sway)
|
||||
|
||||
### Feature Comparison
|
||||
|
||||
| Feature | Hyprland | Sway | Notes |
|
||||
|---------|----------|------|-------|
|
||||
| Activity Detection | ✅ | ✅ | Uses `hyprctl` vs `swaymsg` |
|
||||
| Idle Monitoring | ✅ | ✅ | Uses `hypridle` vs `swayidle` |
|
||||
| Visual Enhancements | ✅ | ❌ | Disabled by design for Sway |
|
||||
| Desktop Theming | ✅ | ❌ | Config modifications disabled |
|
||||
| Notifications | ✅ | ✅ | Level-up and achievement alerts |
|
||||
| WebSocket API | ✅ | ✅ | Full dashboard functionality |
|
||||
| Database Tracking | ✅ | ✅ | Identical functionality |
|
||||
|
||||
### Runtime Detection
|
||||
|
||||
The system automatically detects your window manager at startup:
|
||||
|
||||
1. **Environment Variables**: Checks `$HYPRLAND_INSTANCE_SIGNATURE` and `$SWAYSOCK`
|
||||
2. **Process Detection**: Falls back to checking available commands
|
||||
3. **Graceful Degradation**: Uses appropriate implementations or fallbacks
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Initial Setup
|
||||
1. `dart pub get` - Install dependencies
|
||||
2. `dart run bin/xp_nix.dart` - Start server (auto-detects window manager)
|
||||
3. Visit `http://localhost:8080` for dashboard
|
||||
|
||||
### Making Changes
|
||||
1. Run tests: `dart test`
|
||||
2. Check analysis: `dart analyze`
|
||||
3. Test with custom database: `dart run bin/xp_nix.dart --db test.db`
|
||||
4. Build dashboard: Use `build` command at runtime command `flutter build web` in `../xp_dashboard`
|
||||
|
||||
### Configuration Changes
|
||||
- Edit `config/xp_config.json` for XP rules and achievements
|
||||
- Use `refresh` command to reload configuration
|
||||
- Test theme changes with `test [level]` command
|
||||
- Use `restore` command if desktop theme issues occur
|
||||
|
||||
## Roadmap Integration
|
||||
|
||||
The project roadmap includes ambitious enhancements:
|
||||
- **Cursor-following XP numbers** - RPG-style floating damage numbers
|
||||
- **Dual currency shop system** - XP points and real-world reward tokens
|
||||
- **Enhanced celebrations** - Full-screen level-up animations
|
||||
- **Environmental feedback** - Focus-based ambient changes
|
||||
- **Real-world purchase integration** - Automated reward fulfillment
|
||||
|
||||
Current architecture supports these extensions through:
|
||||
- Modular desktop enhancement system
|
||||
- Extensible notification framework
|
||||
- Configurable reward and achievement system
|
||||
- Database schema designed for expansion
|
||||
|
||||
+72
-10
@@ -7,15 +7,23 @@ import 'package:xp_nix/src/config/config_manager.dart';
|
||||
import 'package:xp_nix/src/logging/logger.dart';
|
||||
import 'package:xp_nix/src/web/dashboard_server.dart';
|
||||
import 'package:xp_nix/src/database/database_manager.dart';
|
||||
import 'package:xp_nix/src/detectors/idle_monitor.dart';
|
||||
import 'package:xp_nix/src/providers/system_time_provider.dart';
|
||||
import 'package:xp_nix/src/enhancers/hyprland_enhancer.dart';
|
||||
import 'package:xp_nix/src/build/dashboard_builder.dart';
|
||||
// Window manager detection and factories
|
||||
import 'package:xp_nix/src/detectors/window_manager_detector.dart';
|
||||
import 'package:xp_nix/src/factories/idle_monitor_factory.dart';
|
||||
import 'package:xp_nix/src/factories/activity_detector_factory.dart';
|
||||
import 'package:xp_nix/src/factories/desktop_enhancer_factory.dart';
|
||||
// Interfaces
|
||||
import 'package:xp_nix/src/interfaces/i_idle_monitor.dart';
|
||||
import 'package:xp_nix/src/interfaces/i_activity_detector.dart';
|
||||
import 'package:xp_nix/src/interfaces/i_desktop_enhancer.dart';
|
||||
|
||||
late final Database _db;
|
||||
late final IdleMonitor _idleMonitor;
|
||||
late final IIdleMonitor _idleMonitor;
|
||||
late final SystemTimeProvider _timeProvider;
|
||||
late final HyprlandEnhancer _desktopEnhancer;
|
||||
late final IDesktopEnhancer _desktopEnhancer;
|
||||
late final IActivityDetector? _activityDetector;
|
||||
late final ProductivityMonitor _monitor;
|
||||
late final DashboardServer _dashboardServer;
|
||||
|
||||
@@ -48,7 +56,10 @@ void main(List<String> args) async {
|
||||
case '--help':
|
||||
case '-h':
|
||||
print('''
|
||||
XP Nix Server
|
||||
XP Nix Server - Productivity Tracking with Gamification
|
||||
|
||||
A productivity tracking system that works with both Hyprland and Sway window managers.
|
||||
Automatically detects your environment and adapts functionality accordingly.
|
||||
|
||||
Usage: dart xp_nix.dart [options]
|
||||
|
||||
@@ -57,9 +68,18 @@ Options:
|
||||
--port PORT Server port (default: 8080)
|
||||
--help, -h Show this help message
|
||||
|
||||
Supported Window Managers:
|
||||
• Hyprland: Full visual effects, idle monitoring, activity detection
|
||||
• Sway: Activity detection, idle monitoring (no visual effects)
|
||||
• Detection: Automatic via environment variables
|
||||
|
||||
Examples:
|
||||
dart xp_nix.dart
|
||||
dart xp_nix.dart --db test_data.db --port 8081
|
||||
|
||||
Requirements:
|
||||
• Hyprland: hypridle, hyprctl
|
||||
• Sway: swayidle, swaymsg
|
||||
''');
|
||||
exit(0);
|
||||
}
|
||||
@@ -74,12 +94,54 @@ Examples:
|
||||
// Initialize configuration manager
|
||||
await ConfigManager.instance.initialize();
|
||||
|
||||
// Detect window manager and display information
|
||||
final windowManager = WindowManagerDetector.instance.detect();
|
||||
final detectionInfo = WindowManagerDetector.instance.getDetectionInfo();
|
||||
|
||||
print('🖼️ Detected window manager: ${windowManager.toString()}');
|
||||
print(' Detection method: ${detectionInfo['detection_method']}');
|
||||
|
||||
// Log additional environment info
|
||||
Logger.info('Window manager detection: $detectionInfo');
|
||||
|
||||
_db = sqlite3.open(dbPath);
|
||||
|
||||
// Create production dependencies
|
||||
_idleMonitor = IdleMonitor();
|
||||
_timeProvider = SystemTimeProvider();
|
||||
_desktopEnhancer = HyprlandEnhancer();
|
||||
// Create window manager-specific dependencies using factories
|
||||
try {
|
||||
_idleMonitor = IdleMonitorFactory.create(windowManager);
|
||||
_desktopEnhancer = DesktopEnhancerFactory.create(windowManager);
|
||||
_activityDetector = ActivityDetectorFactory.create(windowManager);
|
||||
_timeProvider = SystemTimeProvider();
|
||||
|
||||
// Validate requirements for the detected window manager
|
||||
final idleSupported = await IdleMonitorFactory.validateRequirements(windowManager);
|
||||
final activitySupported = await ActivityDetectorFactory.validateRequirements(windowManager);
|
||||
final enhancerSupported = await DesktopEnhancerFactory.validateRequirements(windowManager);
|
||||
|
||||
print('💡 Component availability:');
|
||||
print(' Idle monitoring: ${idleSupported ? '✅' : '❌'}');
|
||||
print(' Activity detection: ${activitySupported ? '✅' : '❌'}');
|
||||
print(' Visual enhancements: ${enhancerSupported ? '✅' : '❌'}');
|
||||
|
||||
if (!idleSupported) {
|
||||
print(' ⚠️ Idle monitoring may not work properly');
|
||||
}
|
||||
if (!activitySupported) {
|
||||
print(' ⚠️ Activity detection will fall back to legacy polling');
|
||||
}
|
||||
if (!enhancerSupported) {
|
||||
print(' ⚠️ Visual enhancements may be limited');
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
print('❌ Error creating window manager components: $e');
|
||||
print(' Falling back to legacy mode...');
|
||||
|
||||
// Fallback to original Hyprland implementations (import at top of file)
|
||||
// Note: This requires adding imports back if needed
|
||||
Logger.error('Component creation failed, falling back to legacy mode: $e');
|
||||
exit(1); // For now, exit on component creation failure
|
||||
}
|
||||
|
||||
// Create monitor with dependency injection
|
||||
_monitor = ProductivityMonitor(
|
||||
@@ -87,7 +149,7 @@ Examples:
|
||||
idleMonitor: _idleMonitor,
|
||||
timeProvider: _timeProvider,
|
||||
desktopEnhancer: _desktopEnhancer,
|
||||
// No activity detector provided - will use legacy polling mode
|
||||
activityDetector: _activityDetector, // May be null for fallback to legacy polling
|
||||
);
|
||||
|
||||
_dashboardServer = DashboardServer.withDatabase(DatabaseManager(_db));
|
||||
|
||||
Generated
-61
@@ -1,61 +0,0 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1749285348,
|
||||
"narHash": "sha256-frdhQvPbmDYaScPFiCnfdh3B/Vh81Uuoo0w5TkWmmjU=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3e3afe5174c561dee0df6f2c2b2236990146329f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
{
|
||||
description = "Simple flutter and dart flake, creating the fart stack";
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||
|
||||
outputs = {
|
||||
flake-utils,
|
||||
nixpkgs,
|
||||
...
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (system: let
|
||||
lib = nixpkgs.lib;
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [
|
||||
"android-studio-stable"
|
||||
];
|
||||
};
|
||||
in {
|
||||
devShell = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
flutter
|
||||
dart
|
||||
clang
|
||||
cmake
|
||||
ninja
|
||||
pkg-config
|
||||
gtk3
|
||||
pcre
|
||||
libepoxy
|
||||
# For drift
|
||||
sqlite
|
||||
|
||||
# Dev deps
|
||||
libuuid # for mount.pc
|
||||
xorg.libXdmcp.dev
|
||||
# python310Packages.libselinux.dev # for libselinux.pc
|
||||
libsepol.dev
|
||||
libthai.dev
|
||||
libdatrie.dev
|
||||
libxkbcommon.dev
|
||||
dbus.dev
|
||||
at-spi2-core.dev
|
||||
xorg.libXtst.out
|
||||
pcre2.dev
|
||||
jdk11
|
||||
android-studio
|
||||
android-tools
|
||||
];
|
||||
LD_LIBRARY_PATH = "${pkgs.sqlite.out}/lib";
|
||||
shellHook = ''
|
||||
export PATH="$PATH":"$HOME/.pub-cache/bin"
|
||||
echo -e "\e[44m \e[0m"
|
||||
echo " FartStack Enabled 💨"
|
||||
echo -e "\e[44m \e[0m"
|
||||
'';
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import '../interfaces/i_activity_detector.dart';
|
||||
import '../models/activity_event.dart';
|
||||
|
||||
/// Sway-specific activity detector that polls the focused window
|
||||
class SwayActivityDetector implements IActivityDetector {
|
||||
final StreamController<ActivityEvent> _activityController = StreamController<ActivityEvent>.broadcast();
|
||||
Timer? _pollTimer;
|
||||
String? _lastActiveWindow;
|
||||
String? _lastActiveWindowTitle;
|
||||
DateTime? _lastActivityTime;
|
||||
bool _isRunning = false;
|
||||
|
||||
@override
|
||||
Stream<ActivityEvent> get activityStream => _activityController.stream;
|
||||
|
||||
@override
|
||||
Future<void> start() async {
|
||||
if (_isRunning) return;
|
||||
|
||||
_isRunning = true;
|
||||
_pollTimer = Timer.periodic(Duration(seconds: 5), (_) => _pollActivity());
|
||||
print('🔍 Started Sway activity detection...');
|
||||
}
|
||||
|
||||
@override
|
||||
void stop() {
|
||||
_isRunning = false;
|
||||
_pollTimer?.cancel();
|
||||
_pollTimer = null;
|
||||
|
||||
// Flush any remaining activity
|
||||
if (_lastActiveWindow != null && _lastActivityTime != null) {
|
||||
final duration = DateTime.now().difference(_lastActivityTime!).inSeconds;
|
||||
if (duration >= 10) {
|
||||
_emitActivityEvent(_lastActiveWindow!, _lastActiveWindowTitle ?? '', duration);
|
||||
}
|
||||
}
|
||||
|
||||
_activityController.close();
|
||||
print('🛑 Stopped Sway activity detection');
|
||||
}
|
||||
|
||||
Future<void> _pollActivity() async {
|
||||
try {
|
||||
final result = await Process.run('swaymsg', ['-t', 'get_tree']);
|
||||
if (result.exitCode != 0) {
|
||||
print('Warning: swaymsg get_tree failed with exit code ${result.exitCode}');
|
||||
return;
|
||||
}
|
||||
|
||||
final treeData = jsonDecode(result.stdout);
|
||||
final focusedNode = _findFocusedNode(treeData);
|
||||
|
||||
if (focusedNode == null) {
|
||||
// No focused window found
|
||||
return;
|
||||
}
|
||||
|
||||
final currentApp = _extractAppId(focusedNode);
|
||||
final currentWindowTitle = _extractWindowTitle(focusedNode);
|
||||
final now = DateTime.now();
|
||||
|
||||
// If this is a different activity from the last one, emit the previous activity
|
||||
if (_lastActiveWindow != null &&
|
||||
(_lastActiveWindow != currentApp || _lastActiveWindowTitle != currentWindowTitle)) {
|
||||
final duration = now.difference(_lastActivityTime ?? now).inSeconds;
|
||||
if (duration >= 10) {
|
||||
_emitActivityEvent(_lastActiveWindow!, _lastActiveWindowTitle ?? '', duration);
|
||||
}
|
||||
}
|
||||
|
||||
// Update current activity
|
||||
_lastActiveWindow = currentApp;
|
||||
_lastActiveWindowTitle = currentWindowTitle;
|
||||
_lastActivityTime = now;
|
||||
} catch (e) {
|
||||
print('Error polling Sway activity: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively find the focused node in the Sway tree
|
||||
Map<String, dynamic>? _findFocusedNode(Map<String, dynamic> node) {
|
||||
// Check if this node is focused
|
||||
if (node['focused'] == true) {
|
||||
return node;
|
||||
}
|
||||
|
||||
// Check child nodes
|
||||
final nodes = node['nodes'] as List<dynamic>? ?? [];
|
||||
for (final childNode in nodes) {
|
||||
if (childNode is Map<String, dynamic>) {
|
||||
final focused = _findFocusedNode(childNode);
|
||||
if (focused != null) {
|
||||
return focused;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check floating nodes
|
||||
final floatingNodes = node['floating_nodes'] as List<dynamic>? ?? [];
|
||||
for (final floatingNode in floatingNodes) {
|
||||
if (floatingNode is Map<String, dynamic>) {
|
||||
final focused = _findFocusedNode(floatingNode);
|
||||
if (focused != null) {
|
||||
return focused;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Extract application ID from a Sway node
|
||||
String _extractAppId(Map<String, dynamic> node) {
|
||||
// In Sway, the application identifier can be in different fields depending on the application type
|
||||
|
||||
// For Wayland applications, use app_id
|
||||
final appId = node['app_id'] as String?;
|
||||
if (appId != null && appId.isNotEmpty) {
|
||||
return appId;
|
||||
}
|
||||
|
||||
// For X11 applications, use window_properties.class
|
||||
final windowProperties = node['window_properties'] as Map<String, dynamic>?;
|
||||
if (windowProperties != null) {
|
||||
final windowClass = windowProperties['class'] as String?;
|
||||
if (windowClass != null && windowClass.isNotEmpty) {
|
||||
return windowClass;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to window type or node type
|
||||
final type = node['type'] as String?;
|
||||
if (type != null && type != 'con') {
|
||||
return type;
|
||||
}
|
||||
|
||||
// Last resort fallback
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
/// Extract window title from a Sway node
|
||||
String _extractWindowTitle(Map<String, dynamic> node) {
|
||||
// Try the name field first
|
||||
final name = node['name'] as String?;
|
||||
if (name != null && name.isNotEmpty) {
|
||||
return name;
|
||||
}
|
||||
|
||||
// For X11 applications, try window_properties.title
|
||||
final windowProperties = node['window_properties'] as Map<String, dynamic>?;
|
||||
if (windowProperties != null) {
|
||||
final title = windowProperties['title'] as String?;
|
||||
if (title != null && title.isNotEmpty) {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to app_id if name is empty
|
||||
final appId = node['app_id'] as String?;
|
||||
if (appId != null && appId.isNotEmpty) {
|
||||
return appId;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
void _emitActivityEvent(String application, String title, int durationSeconds) {
|
||||
final event = ActivityEvent(
|
||||
type: ActivityEventType.categorize(applicationId: application, applicationTitle: title),
|
||||
application: application,
|
||||
metadata: title,
|
||||
timestamp: DateTime.now(),
|
||||
);
|
||||
|
||||
_activityController.add(event);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ActivityEvent> getCurrentActivity() {
|
||||
return _activityController.stream.single;
|
||||
}
|
||||
|
||||
/// Check if swaymsg is available on the system
|
||||
static Future<bool> isAvailable() async {
|
||||
try {
|
||||
final result = await Process.run('which', ['swaymsg']);
|
||||
return result.exitCode == 0;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get Sway version information
|
||||
static Future<String?> getVersion() async {
|
||||
try {
|
||||
final result = await Process.run('swaymsg', ['-t', 'get_version']);
|
||||
if (result.exitCode == 0) {
|
||||
final versionData = jsonDecode(result.stdout);
|
||||
return versionData['human_readable'] as String? ?? 'Sway (version unknown)';
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Test the Sway connection and get basic info
|
||||
static Future<Map<String, dynamic>?> getSwayInfo() async {
|
||||
try {
|
||||
final result = await Process.run('swaymsg', ['-t', 'get_version']);
|
||||
if (result.exitCode == 0) {
|
||||
return jsonDecode(result.stdout) as Map<String, dynamic>;
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import '../interfaces/i_idle_monitor.dart';
|
||||
|
||||
/// Sway-specific idle monitor using swayidle
|
||||
class SwayIdleMonitor implements IIdleMonitor {
|
||||
static const String _idleLightStartKey = 'SWAY_IDLE_LIGHT_START';
|
||||
static const String _idleDeepStartKey = 'SWAY_IDLE_DEEP_START';
|
||||
static const String _idleEndKey = 'SWAY_IDLE_END';
|
||||
|
||||
Process? _swayidleProcess;
|
||||
final StreamController<IdleStatus> _idleStateController = StreamController<IdleStatus>.broadcast();
|
||||
IdleStatus _currentIdleStatus = IdleStatus.active;
|
||||
|
||||
@override
|
||||
Stream<IdleStatus> get idleStateStream => _idleStateController.stream;
|
||||
|
||||
@override
|
||||
IdleStatus get status => _currentIdleStatus;
|
||||
|
||||
@override
|
||||
Future<void> start() async {
|
||||
// Start in active state
|
||||
_currentIdleStatus = IdleStatus.active;
|
||||
_idleStateController.add(IdleStatus.active);
|
||||
|
||||
// Build swayidle command arguments
|
||||
final swayidleArgs = [
|
||||
'-w', // Wait for command to finish before continuing
|
||||
'timeout', '20', 'echo "$_idleLightStartKey"',
|
||||
'resume', 'echo "$_idleEndKey"',
|
||||
'timeout', '120', 'echo "$_idleDeepStartKey"',
|
||||
'resume', 'echo "$_idleEndKey"',
|
||||
];
|
||||
|
||||
try {
|
||||
// Start swayidle with our configuration
|
||||
_swayidleProcess = await Process.start('swayidle', swayidleArgs);
|
||||
|
||||
// Listen to swayidle output
|
||||
_swayidleProcess!.stdout
|
||||
.transform(utf8.decoder)
|
||||
.transform(LineSplitter())
|
||||
.listen(_handleSwayidleOutput);
|
||||
|
||||
_swayidleProcess!.stderr
|
||||
.transform(utf8.decoder)
|
||||
.transform(LineSplitter())
|
||||
.listen((line) {
|
||||
if (line.isNotEmpty) {
|
||||
print('SwayIdle stderr: $line');
|
||||
}
|
||||
});
|
||||
|
||||
print('🔄 Started Sway idle monitoring with swayidle');
|
||||
} catch (e) {
|
||||
print('❌ Failed to start swayidle: $e');
|
||||
print(' Make sure swayidle is installed and available in PATH');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSwayidleOutput(String line) {
|
||||
if (line.isEmpty) return;
|
||||
|
||||
switch (line) {
|
||||
case String s when s.contains(_idleLightStartKey):
|
||||
_idleStateController.add(IdleStatus.lightIdle);
|
||||
_currentIdleStatus = IdleStatus.lightIdle;
|
||||
print('🌙 User went light idle (Sway)');
|
||||
case String s when s.contains(_idleDeepStartKey):
|
||||
_idleStateController.add(IdleStatus.deepIdle);
|
||||
_currentIdleStatus = IdleStatus.deepIdle;
|
||||
print('😴 User went deep idle (Sway)');
|
||||
case String s when s.contains(_idleEndKey):
|
||||
_idleStateController.add(IdleStatus.active);
|
||||
_currentIdleStatus = IdleStatus.active;
|
||||
print('⚡ User went active (Sway)');
|
||||
default:
|
||||
// Handle other swayidle output if needed
|
||||
if (line.trim().isNotEmpty) {
|
||||
print('SwayIdle output: $line');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void stop() {
|
||||
try {
|
||||
_swayidleProcess?.kill();
|
||||
_swayidleProcess = null;
|
||||
print('🛑 Stopped Sway idle monitoring');
|
||||
} catch (e) {
|
||||
print('Warning: Error stopping swayidle process: $e');
|
||||
}
|
||||
|
||||
try {
|
||||
_idleStateController.close();
|
||||
} catch (e) {
|
||||
print('Warning: Error closing idle state controller: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if swayidle is available on the system
|
||||
static Future<bool> isAvailable() async {
|
||||
try {
|
||||
final result = await Process.run('which', ['swayidle']);
|
||||
return result.exitCode == 0;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get swayidle version information
|
||||
static Future<String?> getVersion() async {
|
||||
try {
|
||||
final result = await Process.run('swayidle', ['-h']);
|
||||
if (result.exitCode == 0) {
|
||||
// swayidle doesn't have a version flag, but -h should work
|
||||
return 'swayidle (available)';
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import 'dart:io';
|
||||
|
||||
/// Supported window managers
|
||||
enum WindowManager {
|
||||
hyprland,
|
||||
sway,
|
||||
unknown;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return switch (this) {
|
||||
WindowManager.hyprland => 'Hyprland',
|
||||
WindowManager.sway => 'Sway',
|
||||
WindowManager.unknown => 'Unknown',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Singleton class for detecting the current window manager at runtime
|
||||
class WindowManagerDetector {
|
||||
static WindowManagerDetector? _instance;
|
||||
static WindowManagerDetector get instance => _instance ??= WindowManagerDetector._();
|
||||
|
||||
WindowManagerDetector._();
|
||||
|
||||
WindowManager? _detectedWindowManager;
|
||||
|
||||
/// Detect the current window manager based on environment variables
|
||||
WindowManager detect() {
|
||||
if (_detectedWindowManager != null) {
|
||||
return _detectedWindowManager!;
|
||||
}
|
||||
|
||||
final env = Platform.environment;
|
||||
|
||||
// Check for Hyprland signature
|
||||
if (env.containsKey('HYPRLAND_INSTANCE_SIGNATURE') && env['HYPRLAND_INSTANCE_SIGNATURE']!.isNotEmpty) {
|
||||
_detectedWindowManager = WindowManager.hyprland;
|
||||
return _detectedWindowManager!;
|
||||
}
|
||||
|
||||
// Check for Sway socket
|
||||
if (env.containsKey('SWAYSOCK') && env['SWAYSOCK']!.isNotEmpty) {
|
||||
_detectedWindowManager = WindowManager.sway;
|
||||
return _detectedWindowManager!;
|
||||
}
|
||||
|
||||
// Fallback: Try to detect by checking running processes
|
||||
_detectedWindowManager = _detectByProcess();
|
||||
return _detectedWindowManager!;
|
||||
}
|
||||
|
||||
/// Fallback detection by checking running processes
|
||||
WindowManager _detectByProcess() {
|
||||
try {
|
||||
// Check if hyprctl is available and working
|
||||
final hyprResult = Process.runSync('which', ['hyprctl']);
|
||||
if (hyprResult.exitCode == 0) {
|
||||
final hyprTest = Process.runSync('hyprctl', ['version']);
|
||||
if (hyprTest.exitCode == 0) {
|
||||
return WindowManager.hyprland;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if swaymsg is available and working
|
||||
final swayResult = Process.runSync('which', ['swaymsg']);
|
||||
if (swayResult.exitCode == 0) {
|
||||
final swayTest = Process.runSync('swaymsg', ['-t', 'get_version']);
|
||||
if (swayTest.exitCode == 0) {
|
||||
return WindowManager.sway;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors in fallback detection
|
||||
}
|
||||
|
||||
return WindowManager.unknown;
|
||||
}
|
||||
|
||||
/// Get information about the detected window manager
|
||||
Map<String, dynamic> getDetectionInfo() {
|
||||
final windowManager = detect();
|
||||
final env = Platform.environment;
|
||||
|
||||
return {
|
||||
'detected_window_manager': windowManager.toString(),
|
||||
'detection_method': _getDetectionMethod(),
|
||||
'environment_variables': {
|
||||
'HYPRLAND_INSTANCE_SIGNATURE': env['HYPRLAND_INSTANCE_SIGNATURE'],
|
||||
'SWAYSOCK': env['SWAYSOCK'],
|
||||
'WAYLAND_DISPLAY': env['WAYLAND_DISPLAY'],
|
||||
'XDG_CURRENT_DESKTOP': env['XDG_CURRENT_DESKTOP'],
|
||||
'XDG_SESSION_TYPE': env['XDG_SESSION_TYPE'],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
String _getDetectionMethod() {
|
||||
final env = Platform.environment;
|
||||
|
||||
if (env.containsKey('HYPRLAND_INSTANCE_SIGNATURE') && env['HYPRLAND_INSTANCE_SIGNATURE']!.isNotEmpty) {
|
||||
return 'HYPRLAND_INSTANCE_SIGNATURE environment variable';
|
||||
}
|
||||
|
||||
if (env.containsKey('SWAYSOCK') && env['SWAYSOCK']!.isNotEmpty) {
|
||||
return 'SWAYSOCK environment variable';
|
||||
}
|
||||
|
||||
return 'Process detection fallback';
|
||||
}
|
||||
|
||||
/// Reset detection (useful for testing)
|
||||
void reset() {
|
||||
_detectedWindowManager = null;
|
||||
}
|
||||
|
||||
/// Check if the window manager supports visual enhancements
|
||||
bool supportsVisualEnhancements(WindowManager windowManager) {
|
||||
return switch (windowManager) {
|
||||
WindowManager.hyprland => true,
|
||||
WindowManager.sway => false, // As requested: no visual enhancements for Sway
|
||||
WindowManager.unknown => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// Check if the window manager supports idle monitoring
|
||||
bool supportsIdleMonitoring(WindowManager windowManager) {
|
||||
return switch (windowManager) {
|
||||
WindowManager.hyprland => true,
|
||||
WindowManager.sway => true,
|
||||
WindowManager.unknown => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// Check if the window manager supports activity detection
|
||||
bool supportsActivityDetection(WindowManager windowManager) {
|
||||
return switch (windowManager) {
|
||||
WindowManager.hyprland => true,
|
||||
WindowManager.sway => true,
|
||||
WindowManager.unknown => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import 'dart:io';
|
||||
import '../interfaces/i_desktop_enhancer.dart';
|
||||
|
||||
/// Sway desktop enhancer with minimal visual changes
|
||||
///
|
||||
/// As requested, this implementation does not perform aesthetic desktop config changes
|
||||
/// for Sway systems. It maintains API compatibility while providing basic notifications.
|
||||
class SwayEnhancer implements IDesktopEnhancer {
|
||||
String _currentTheme = 'default';
|
||||
|
||||
@override
|
||||
Future<void> celebrateLevelUp(int level) async {
|
||||
print('🎉 Celebrating level up to $level! (Sway)');
|
||||
|
||||
// Update internal theme tracking for API compatibility
|
||||
_updateThemeForLevel(level);
|
||||
|
||||
// Send celebration notification via notify-send
|
||||
try {
|
||||
await Process.run('notify-send', [
|
||||
'🎉 LEVEL UP!',
|
||||
'Welcome to Level $level!\nKeep up the great productivity!',
|
||||
'--urgency=normal',
|
||||
'--expire-time=5000',
|
||||
'--app-name=XP Nix',
|
||||
]);
|
||||
} catch (e) {
|
||||
print('Could not send level up notification: $e');
|
||||
}
|
||||
|
||||
// Optional: Try to send via Sway notification daemon if available
|
||||
await _trySwayNotification(level);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> applyThemeForLevel(int level) async {
|
||||
// Update internal theme tracking only
|
||||
_updateThemeForLevel(level);
|
||||
|
||||
print('🎨 Theme updated to $_currentTheme for level $level (Sway - visual changes disabled)');
|
||||
|
||||
// No actual visual changes are applied for Sway as requested
|
||||
// This maintains compatibility with the API while respecting the no-visual-changes requirement
|
||||
}
|
||||
|
||||
void _updateThemeForLevel(int level) {
|
||||
if (level >= 25) {
|
||||
_currentTheme = 'legendary';
|
||||
} else if (level >= 15) {
|
||||
_currentTheme = 'master';
|
||||
} else if (level >= 10) {
|
||||
_currentTheme = 'advanced';
|
||||
} else if (level >= 5) {
|
||||
_currentTheme = 'intermediate';
|
||||
} else {
|
||||
_currentTheme = 'default';
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to send notification via Sway-specific methods if available
|
||||
Future<void> _trySwayNotification(int level) async {
|
||||
try {
|
||||
// Check if mako (Sway notification daemon) is running
|
||||
final makoCheck = await Process.run('pgrep', ['mako']);
|
||||
if (makoCheck.exitCode == 0) {
|
||||
// Use makoctl if available for enhanced notifications
|
||||
final makoctl = await Process.run('which', ['makoctl']);
|
||||
if (makoctl.exitCode == 0) {
|
||||
// Could add mako-specific notification features here if desired
|
||||
print('📱 Mako notification daemon detected');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Silently ignore errors in optional notification enhancements
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String getCurrentThemeInfo() {
|
||||
return 'Theme: $_currentTheme (Sway - visual effects disabled)';
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> refreshBaseConfig() async {
|
||||
print('🔄 Refresh base config requested (Sway - no config changes applied)');
|
||||
// No-op for Sway as we don't modify configs
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> restoreBackup() async {
|
||||
print('🔧 Restore backup requested (Sway - no backups to restore)');
|
||||
// No-op for Sway as we don't create backups
|
||||
}
|
||||
|
||||
/// Get information about Sway enhancer capabilities
|
||||
Map<String, dynamic> getCapabilityInfo() {
|
||||
return {
|
||||
'window_manager': 'Sway',
|
||||
'visual_enhancements': false,
|
||||
'notifications': true,
|
||||
'config_backup': false,
|
||||
'theme_tracking': true,
|
||||
'current_theme': _currentTheme,
|
||||
'supported_features': [
|
||||
'Level-up notifications',
|
||||
'Theme name tracking',
|
||||
'API compatibility',
|
||||
],
|
||||
'disabled_features': [
|
||||
'Visual effects',
|
||||
'Config modifications',
|
||||
'Backup/restore',
|
||||
'Desktop theming',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/// Check if notification systems are available
|
||||
static Future<Map<String, bool>> checkNotificationAvailability() async {
|
||||
final availability = <String, bool>{};
|
||||
|
||||
try {
|
||||
// Check for notify-send (libnotify)
|
||||
final notifySend = await Process.run('which', ['notify-send']);
|
||||
availability['notify-send'] = notifySend.exitCode == 0;
|
||||
} catch (e) {
|
||||
availability['notify-send'] = false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check for mako (Sway notification daemon)
|
||||
final mako = await Process.run('pgrep', ['mako']);
|
||||
availability['mako'] = mako.exitCode == 0;
|
||||
} catch (e) {
|
||||
availability['mako'] = false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check for dunst (alternative notification daemon)
|
||||
final dunst = await Process.run('pgrep', ['dunst']);
|
||||
availability['dunst'] = dunst.exitCode == 0;
|
||||
} catch (e) {
|
||||
availability['dunst'] = false;
|
||||
}
|
||||
|
||||
return availability;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import '../interfaces/i_activity_detector.dart';
|
||||
import '../detectors/hyprland_activity_detector.dart';
|
||||
import '../detectors/sway_activity_detector.dart';
|
||||
import '../detectors/window_manager_detector.dart';
|
||||
import '../testing/mock_activity_detector.dart';
|
||||
import 'dart:io';
|
||||
|
||||
/// Factory for creating appropriate activity detector implementations based on the window manager
|
||||
class ActivityDetectorFactory {
|
||||
/// Create an activity detector based on the detected window manager
|
||||
static IActivityDetector? create([WindowManager? windowManager]) {
|
||||
windowManager ??= WindowManagerDetector.instance.detect();
|
||||
|
||||
return switch (windowManager) {
|
||||
WindowManager.hyprland => HyprlandActivityDetector(), // Existing Hyprland implementation
|
||||
WindowManager.sway => SwayActivityDetector(), // New Sway implementation
|
||||
WindowManager.unknown => _createFallbackDetector(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Create a mock activity detector for testing
|
||||
static IActivityDetector createMock() {
|
||||
return MockActivityDetector();
|
||||
}
|
||||
|
||||
/// Create an activity detector for a specific window manager (useful for testing)
|
||||
static IActivityDetector? createForWindowManager(WindowManager windowManager) {
|
||||
return create(windowManager);
|
||||
}
|
||||
|
||||
/// Create a fallback activity detector when window manager is unknown
|
||||
static IActivityDetector? _createFallbackDetector() {
|
||||
print('⚠️ Unknown window manager detected for activity detection');
|
||||
print(' Activity detection will be disabled - only legacy polling mode will be available');
|
||||
|
||||
// Return null to indicate activity detection is not available
|
||||
// This will cause the ProductivityMonitor to fall back to legacy polling mode
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Get information about available activity detector implementations
|
||||
static Map<String, dynamic> getAvailableImplementations() {
|
||||
return {
|
||||
'hyprland': {
|
||||
'class': 'HyprlandActivityDetector',
|
||||
'description': 'Uses hyprctl activewindow for activity detection',
|
||||
'requirements': ['hyprctl'],
|
||||
'polling_interval': '5 seconds',
|
||||
},
|
||||
'sway': {
|
||||
'class': 'SwayActivityDetector',
|
||||
'description': 'Uses swaymsg get_tree for activity detection',
|
||||
'requirements': ['swaymsg'],
|
||||
'polling_interval': '5 seconds',
|
||||
},
|
||||
'mock': {
|
||||
'class': 'MockActivityDetector',
|
||||
'description': 'Controllable mock for testing',
|
||||
'requirements': [],
|
||||
'polling_interval': 'configurable',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// Check if activity detection is supported for the given window manager
|
||||
static bool isSupported(WindowManager windowManager) {
|
||||
return WindowManagerDetector.instance.supportsActivityDetection(windowManager);
|
||||
}
|
||||
|
||||
/// Validate that required tools are available for the window manager
|
||||
static Future<bool> validateRequirements(WindowManager windowManager) async {
|
||||
return switch (windowManager) {
|
||||
WindowManager.hyprland => await _validateHyprlandRequirements(),
|
||||
WindowManager.sway => await SwayActivityDetector.isAvailable(),
|
||||
WindowManager.unknown => false,
|
||||
};
|
||||
}
|
||||
|
||||
static Future<bool> _validateHyprlandRequirements() async {
|
||||
try {
|
||||
final result = await Process.run('which', ['hyprctl']);
|
||||
if (result.exitCode != 0) return false;
|
||||
|
||||
// Test that hyprctl actually works
|
||||
final testResult = await Process.run('hyprctl', ['version']);
|
||||
return testResult.exitCode == 0;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get version information for activity detection tools
|
||||
static Future<Map<String, String?>> getToolVersions() async {
|
||||
final versions = <String, String?>{};
|
||||
|
||||
// Check Hyprland version
|
||||
try {
|
||||
final hyprResult = await Process.run('hyprctl', ['version']);
|
||||
if (hyprResult.exitCode == 0) {
|
||||
final output = hyprResult.stdout as String;
|
||||
final lines = output.split('\n');
|
||||
if (lines.isNotEmpty) {
|
||||
versions['hyprland'] = lines.first.trim();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
versions['hyprland'] = null;
|
||||
}
|
||||
|
||||
// Check Sway version
|
||||
try {
|
||||
final swayVersion = await SwayActivityDetector.getVersion();
|
||||
versions['sway'] = swayVersion;
|
||||
} catch (e) {
|
||||
versions['sway'] = null;
|
||||
}
|
||||
|
||||
return versions;
|
||||
}
|
||||
|
||||
/// Test connectivity to window manager APIs
|
||||
static Future<Map<String, bool>> testConnectivity() async {
|
||||
final connectivity = <String, bool>{};
|
||||
|
||||
// Test Hyprland connectivity
|
||||
try {
|
||||
final hyprResult = await Process.run('hyprctl', ['activewindow', '-j']);
|
||||
connectivity['hyprland'] = hyprResult.exitCode == 0;
|
||||
} catch (e) {
|
||||
connectivity['hyprland'] = false;
|
||||
}
|
||||
|
||||
// Test Sway connectivity
|
||||
try {
|
||||
final swayResult = await Process.run('swaymsg', ['-t', 'get_tree']);
|
||||
connectivity['sway'] = swayResult.exitCode == 0;
|
||||
} catch (e) {
|
||||
connectivity['sway'] = false;
|
||||
}
|
||||
|
||||
return connectivity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import 'dart:io';
|
||||
|
||||
import '../interfaces/i_desktop_enhancer.dart';
|
||||
import '../enhancers/hyprland_enhancer.dart';
|
||||
import '../enhancers/sway_enhancer.dart';
|
||||
import '../detectors/window_manager_detector.dart';
|
||||
import '../testing/mock_desktop_enhancer.dart';
|
||||
|
||||
/// Factory for creating appropriate desktop enhancer implementations based on the window manager
|
||||
class DesktopEnhancerFactory {
|
||||
/// Create a desktop enhancer based on the detected window manager
|
||||
static IDesktopEnhancer create([WindowManager? windowManager]) {
|
||||
windowManager ??= WindowManagerDetector.instance.detect();
|
||||
|
||||
return switch (windowManager) {
|
||||
WindowManager.hyprland => HyprlandEnhancer(), // Existing Hyprland implementation with full visual effects
|
||||
WindowManager.sway => SwayEnhancer(), // New Sway implementation with no visual effects
|
||||
WindowManager.unknown => _createFallbackEnhancer(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Create a mock desktop enhancer for testing
|
||||
static IDesktopEnhancer createMock() {
|
||||
return MockDesktopEnhancer();
|
||||
}
|
||||
|
||||
/// Create a desktop enhancer for a specific window manager (useful for testing)
|
||||
static IDesktopEnhancer createForWindowManager(WindowManager windowManager) {
|
||||
return create(windowManager);
|
||||
}
|
||||
|
||||
/// Create a fallback desktop enhancer when window manager is unknown
|
||||
static IDesktopEnhancer _createFallbackEnhancer() {
|
||||
print('⚠️ Unknown window manager detected, using no-op desktop enhancer');
|
||||
print(' Visual enhancements will be disabled');
|
||||
|
||||
// Use Sway enhancer as fallback since it's no-op
|
||||
return SwayEnhancer();
|
||||
}
|
||||
|
||||
/// Get information about available desktop enhancer implementations
|
||||
static Map<String, dynamic> getAvailableImplementations() {
|
||||
return {
|
||||
'hyprland': {
|
||||
'class': 'HyprlandEnhancer',
|
||||
'description': 'Full visual enhancements for Hyprland',
|
||||
'features': [
|
||||
'Dynamic blur effects',
|
||||
'Progressive shadows',
|
||||
'Animated borders',
|
||||
'Level-based themes',
|
||||
'Config backup/restore',
|
||||
'Real-time visual updates',
|
||||
],
|
||||
'requirements': ['hyprctl'],
|
||||
'visual_enhancements': true,
|
||||
},
|
||||
'sway': {
|
||||
'class': 'SwayEnhancer',
|
||||
'description': 'Minimal enhancer for Sway (notifications only)',
|
||||
'features': ['Level-up notifications', 'Theme name tracking', 'API compatibility'],
|
||||
'requirements': ['notify-send (optional)'],
|
||||
'visual_enhancements': false,
|
||||
},
|
||||
'mock': {
|
||||
'class': 'MockDesktopEnhancer',
|
||||
'description': 'No-op mock for testing',
|
||||
'features': ['API compatibility'],
|
||||
'requirements': [],
|
||||
'visual_enhancements': false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// Check if visual enhancements are supported for the given window manager
|
||||
static bool supportsVisualEnhancements(WindowManager windowManager) {
|
||||
return WindowManagerDetector.instance.supportsVisualEnhancements(windowManager);
|
||||
}
|
||||
|
||||
/// Get the feature set available for a window manager
|
||||
static List<String> getAvailableFeatures(WindowManager windowManager) {
|
||||
final implementations = getAvailableImplementations();
|
||||
|
||||
return switch (windowManager) {
|
||||
WindowManager.hyprland => List<String>.from(implementations['hyprland']['features']),
|
||||
WindowManager.sway => List<String>.from(implementations['sway']['features']),
|
||||
WindowManager.unknown => ['Basic notifications'],
|
||||
};
|
||||
}
|
||||
|
||||
/// Validate that required tools are available for the window manager
|
||||
static Future<bool> validateRequirements(WindowManager windowManager) async {
|
||||
return switch (windowManager) {
|
||||
WindowManager.hyprland => await _validateHyprlandRequirements(),
|
||||
WindowManager.sway => await _validateSwayRequirements(),
|
||||
WindowManager.unknown => true, // Fallback always works
|
||||
};
|
||||
}
|
||||
|
||||
static Future<bool> _validateHyprlandRequirements() async {
|
||||
try {
|
||||
final result = await Process.run('which', ['hyprctl']);
|
||||
if (result.exitCode != 0) return false;
|
||||
|
||||
// Test that hyprctl actually works
|
||||
final testResult = await Process.run('hyprctl', ['version']);
|
||||
return testResult.exitCode == 0;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> _validateSwayRequirements() async {
|
||||
// Sway enhancer has no hard requirements, but check for optional notify-send
|
||||
try {
|
||||
final result = await Process.run('which', ['notify-send']);
|
||||
// Even if notify-send is not available, Sway enhancer still works
|
||||
return true;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get capability information for the current window manager
|
||||
static Future<Map<String, dynamic>> getCapabilityInfo([WindowManager? windowManager]) async {
|
||||
windowManager ??= WindowManagerDetector.instance.detect();
|
||||
|
||||
final enhancer = create(windowManager);
|
||||
final implementations = getAvailableImplementations();
|
||||
|
||||
final info = {
|
||||
'window_manager': windowManager.toString(),
|
||||
'enhancer_class': enhancer.runtimeType.toString(),
|
||||
'visual_enhancements': supportsVisualEnhancements(windowManager),
|
||||
'available_features': getAvailableFeatures(windowManager),
|
||||
'requirements_met': await validateRequirements(windowManager),
|
||||
};
|
||||
|
||||
// Add window manager specific info
|
||||
if (windowManager == WindowManager.sway && enhancer is SwayEnhancer) {
|
||||
info['sway_capabilities'] = enhancer.getCapabilityInfo();
|
||||
info['notification_availability'] = await SwayEnhancer.checkNotificationAvailability();
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import 'dart:io';
|
||||
|
||||
import '../interfaces/i_idle_monitor.dart';
|
||||
import '../detectors/idle_monitor.dart';
|
||||
import '../detectors/sway_idle_monitor.dart';
|
||||
import '../detectors/window_manager_detector.dart';
|
||||
import '../testing/mock_idle_monitor.dart';
|
||||
|
||||
/// Factory for creating appropriate idle monitor implementations based on the window manager
|
||||
class IdleMonitorFactory {
|
||||
/// Create an idle monitor based on the detected window manager
|
||||
static IIdleMonitor create([WindowManager? windowManager]) {
|
||||
windowManager ??= WindowManagerDetector.instance.detect();
|
||||
|
||||
return switch (windowManager) {
|
||||
WindowManager.hyprland => IdleMonitor(), // Existing Hyprland implementation
|
||||
WindowManager.sway => SwayIdleMonitor(), // New Sway implementation
|
||||
WindowManager.unknown => _createFallbackMonitor(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Create a mock idle monitor for testing
|
||||
static IIdleMonitor createMock() {
|
||||
return MockIdleMonitor();
|
||||
}
|
||||
|
||||
/// Create an idle monitor for a specific window manager (useful for testing)
|
||||
static IIdleMonitor createForWindowManager(WindowManager windowManager) {
|
||||
return create(windowManager);
|
||||
}
|
||||
|
||||
/// Create a fallback idle monitor when window manager is unknown
|
||||
static IIdleMonitor _createFallbackMonitor() {
|
||||
print('⚠️ Unknown window manager detected, attempting Hyprland idle monitor as fallback');
|
||||
print(' If this fails, consider running in a supported environment (Hyprland or Sway)');
|
||||
|
||||
// Try Hyprland first as fallback
|
||||
return IdleMonitor();
|
||||
}
|
||||
|
||||
/// Get information about available idle monitor implementations
|
||||
static Map<String, dynamic> getAvailableImplementations() {
|
||||
return {
|
||||
'hyprland': {
|
||||
'class': 'IdleMonitor',
|
||||
'description': 'Uses hypridle for idle detection',
|
||||
'requirements': ['hypridle'],
|
||||
},
|
||||
'sway': {
|
||||
'class': 'SwayIdleMonitor',
|
||||
'description': 'Uses swayidle for idle detection',
|
||||
'requirements': ['swayidle'],
|
||||
},
|
||||
'mock': {'class': 'MockIdleMonitor', 'description': 'Controllable mock for testing', 'requirements': []},
|
||||
};
|
||||
}
|
||||
|
||||
/// Check if idle monitoring is supported for the given window manager
|
||||
static bool isSupported(WindowManager windowManager) {
|
||||
return WindowManagerDetector.instance.supportsIdleMonitoring(windowManager);
|
||||
}
|
||||
|
||||
/// Validate that required tools are available for the window manager
|
||||
static Future<bool> validateRequirements(WindowManager windowManager) async {
|
||||
return switch (windowManager) {
|
||||
WindowManager.hyprland => await _validateHyprlandRequirements(),
|
||||
WindowManager.sway => await SwayIdleMonitor.isAvailable(),
|
||||
WindowManager.unknown => false,
|
||||
};
|
||||
}
|
||||
|
||||
static Future<bool> _validateHyprlandRequirements() async {
|
||||
try {
|
||||
final result = await Process.run('which', ['hypridle']);
|
||||
return result.exitCode == 0;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import '../interfaces/i_window_manager.dart';
|
||||
import '../window_managers/hyprland_manager.dart';
|
||||
import '../window_managers/sway_manager.dart';
|
||||
import '../detectors/window_manager_detector.dart';
|
||||
|
||||
/// Factory for creating appropriate window manager implementations
|
||||
class WindowManagerFactory {
|
||||
/// Create a window manager based on the detected window manager
|
||||
static IWindowManager? create([WindowManager? windowManager]) {
|
||||
windowManager ??= WindowManagerDetector.instance.detect();
|
||||
|
||||
return switch (windowManager) {
|
||||
WindowManager.hyprland => HyprlandManager(),
|
||||
WindowManager.sway => SwayManager(),
|
||||
WindowManager.unknown => null, // No window manager available
|
||||
};
|
||||
}
|
||||
|
||||
/// Create a window manager for a specific type (useful for testing)
|
||||
static IWindowManager? createForWindowManager(WindowManager windowManager) {
|
||||
return create(windowManager);
|
||||
}
|
||||
|
||||
/// Get information about available window manager implementations
|
||||
static Map<String, dynamic> getAvailableImplementations() {
|
||||
return {
|
||||
'hyprland': {
|
||||
'class': 'HyprlandManager',
|
||||
'description': 'Uses hyprctl commands for window management',
|
||||
'commands': ['hyprctl activewindow', 'hyprctl workspaces', 'hyprctl clients'],
|
||||
'requirements': ['hyprctl'],
|
||||
},
|
||||
'sway': {
|
||||
'class': 'SwayManager',
|
||||
'description': 'Uses swaymsg commands for window management',
|
||||
'commands': ['swaymsg -t get_tree', 'swaymsg -t get_workspaces'],
|
||||
'requirements': ['swaymsg'],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// Check if window management is supported for the given window manager
|
||||
static bool isSupported(WindowManager windowManager) {
|
||||
return windowManager != WindowManager.unknown;
|
||||
}
|
||||
|
||||
/// Validate that required tools are available for the window manager
|
||||
static Future<bool> validateRequirements(WindowManager windowManager) async {
|
||||
return switch (windowManager) {
|
||||
WindowManager.hyprland => await _validateHyprlandRequirements(),
|
||||
WindowManager.sway => await _validateSwayRequirements(),
|
||||
WindowManager.unknown => false,
|
||||
};
|
||||
}
|
||||
|
||||
static Future<bool> _validateHyprlandRequirements() async {
|
||||
try {
|
||||
final manager = HyprlandManager();
|
||||
return await manager.isAvailable();
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> _validateSwayRequirements() async {
|
||||
try {
|
||||
final manager = SwayManager();
|
||||
return await manager.isAvailable();
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get version information for available window managers
|
||||
static Future<Map<String, String?>> getVersions() async {
|
||||
final versions = <String, String?>{};
|
||||
|
||||
try {
|
||||
final hyprland = HyprlandManager();
|
||||
versions['hyprland'] = await hyprland.getVersion();
|
||||
} catch (e) {
|
||||
versions['hyprland'] = null;
|
||||
}
|
||||
|
||||
try {
|
||||
final sway = SwayManager();
|
||||
versions['sway'] = await sway.getVersion();
|
||||
} catch (e) {
|
||||
versions['sway'] = null;
|
||||
}
|
||||
|
||||
return versions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/// Interface for window manager operations
|
||||
///
|
||||
/// This interface abstracts window manager-specific operations to provide
|
||||
/// a consistent API for querying window information across different
|
||||
/// window managers (Hyprland, Sway, etc.)
|
||||
abstract interface class IWindowManager {
|
||||
/// Get information about the currently active/focused window
|
||||
///
|
||||
/// Returns a map containing:
|
||||
/// - 'application': Application identifier (class/app_id)
|
||||
/// - 'title': Window title
|
||||
/// - 'pid': Process ID (if available)
|
||||
/// - 'workspace': Current workspace (if available)
|
||||
///
|
||||
/// Returns null if no active window or on error
|
||||
Future<Map<String, dynamic>?> getCurrentWindow();
|
||||
|
||||
/// Get the application identifier of the active window
|
||||
///
|
||||
/// Returns the application class/app_id, or 'unknown' if unavailable
|
||||
Future<String> getActiveApplication();
|
||||
|
||||
/// Get the title of the active window
|
||||
///
|
||||
/// Returns the window title, or empty string if unavailable
|
||||
Future<String> getWindowTitle();
|
||||
|
||||
/// Get the current workspace information
|
||||
///
|
||||
/// Returns a map containing workspace details, or null if unavailable
|
||||
Future<Map<String, dynamic>?> getCurrentWorkspace();
|
||||
|
||||
/// Check if the window manager is available and responding
|
||||
Future<bool> isAvailable();
|
||||
|
||||
/// Get version information about the window manager
|
||||
Future<String?> getVersion();
|
||||
|
||||
/// Get current cursor position in global coordinates
|
||||
///
|
||||
/// Returns a map containing:
|
||||
/// - 'x': X coordinate in pixels
|
||||
/// - 'y': Y coordinate in pixels
|
||||
///
|
||||
/// Returns null if cursor position is unavailable or on error
|
||||
Future<Map<String, int>?> getCursorPosition();
|
||||
|
||||
/// Get the window manager type identifier
|
||||
String get managerType;
|
||||
}
|
||||
@@ -8,12 +8,16 @@ import '../interfaces/i_idle_monitor.dart';
|
||||
import '../interfaces/i_activity_detector.dart';
|
||||
import '../interfaces/i_time_provider.dart';
|
||||
import '../interfaces/i_desktop_enhancer.dart';
|
||||
import '../interfaces/i_window_manager.dart';
|
||||
import '../detectors/zoom_detector.dart';
|
||||
import '../database/database_manager.dart';
|
||||
import '../config/config_manager.dart';
|
||||
import '../logging/logger.dart';
|
||||
import '../notifications/xp_notification_manager.dart';
|
||||
import '../web/websocket_manager.dart';
|
||||
import '../factories/window_manager_factory.dart';
|
||||
import '../detectors/window_manager_detector.dart';
|
||||
import '../overlays/cursor_overlay_manager.dart';
|
||||
|
||||
/// Unified ProductivityMonitor with dependency injection for both production and testing
|
||||
class ProductivityMonitor {
|
||||
@@ -25,6 +29,8 @@ class ProductivityMonitor {
|
||||
late final IDesktopEnhancer _desktopEnhancer;
|
||||
late final XPNotificationManager _xpNotificationManager;
|
||||
late final ZoomDetector? _zoomDetector;
|
||||
late final IWindowManager? _windowManager;
|
||||
late final CursorOverlayManager? _cursorOverlayManager;
|
||||
|
||||
Timer? _pollTimer;
|
||||
String? _lastActiveWindow;
|
||||
@@ -55,6 +61,20 @@ class ProductivityMonitor {
|
||||
_desktopEnhancer = desktopEnhancer;
|
||||
_xpNotificationManager = XPNotificationManager(_dbManager);
|
||||
|
||||
// Initialize window manager for legacy polling mode
|
||||
_windowManager = WindowManagerFactory.create();
|
||||
|
||||
// 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,
|
||||
);
|
||||
} else {
|
||||
_cursorOverlayManager = null;
|
||||
}
|
||||
|
||||
// Initialize zoom detector (only if not in test mode)
|
||||
if (_activityDetector == null) {
|
||||
_zoomDetector = ZoomDetector();
|
||||
@@ -326,6 +346,9 @@ class ProductivityMonitor {
|
||||
// Add bonus XP
|
||||
_dbManager.updateDailyStats(achievement['xp_reward'] as int, 0, 0);
|
||||
|
||||
// Show cursor overlay for achievement bonus XP
|
||||
_cursorOverlayManager?.showXPNumber(achievement['xp_reward'] as int);
|
||||
|
||||
print('🏆 ACHIEVEMENT UNLOCKED: ${achievement['name']}');
|
||||
print(' ${achievement['description']}');
|
||||
print(' Bonus: +${achievement['xp_reward']} XP');
|
||||
@@ -405,6 +428,9 @@ class ProductivityMonitor {
|
||||
_dbManager.saveFocusSession(today, focusMinutes, bonusXP, _timeProvider.now().millisecondsSinceEpoch);
|
||||
_dbManager.updateDailyStats(bonusXP, 0, 0);
|
||||
|
||||
// Show cursor overlay for focus session bonus XP
|
||||
_cursorOverlayManager?.showXPNumber(bonusXP);
|
||||
|
||||
String message = '🎯 Focus session complete: +$bonusXP XP for $focusMinutes min!';
|
||||
if (focusMinutes >= 180) {
|
||||
message = '🔥 LEGENDARY FOCUS! +$bonusXP XP for $focusMinutes min! 🔥';
|
||||
@@ -499,6 +525,49 @@ class ProductivityMonitor {
|
||||
try {
|
||||
if (_idleMonitor.status != IdleStatus.active) return;
|
||||
|
||||
// Use window manager abstraction if available
|
||||
if (_windowManager != null) {
|
||||
final windowInfo = await _windowManager!.getCurrentWindow();
|
||||
if (windowInfo == null) return;
|
||||
|
||||
final currentApp = windowInfo['application'] as String? ?? 'unknown';
|
||||
final currentWindowTitle = windowInfo['title'] as String? ?? '';
|
||||
final now = _timeProvider.now();
|
||||
|
||||
await _checkZoomActivity(now);
|
||||
|
||||
if (_lastActiveWindow != currentApp || _lastActiveWindowTitle != currentWindowTitle) {
|
||||
if (_lastActiveWindow != null && _lastActivityTime != null) {
|
||||
_saveActivityEvent(
|
||||
_lastActiveWindow!,
|
||||
now.difference(_lastActivityTime!).inSeconds,
|
||||
_lastActiveWindowTitle ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
_lastActiveWindow = currentApp;
|
||||
_lastActiveWindowTitle = currentWindowTitle;
|
||||
_lastActivityTime = now;
|
||||
final windowManager = WindowManagerDetector.instance.detect();
|
||||
print('Switched to: $currentApp (via ${windowManager.toString()})');
|
||||
}
|
||||
} else {
|
||||
// Fallback to Hyprland-specific approach if window manager is not available
|
||||
print('Warning: No window manager available, attempting Hyprland fallback');
|
||||
await _pollActivityHyprlandFallback();
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error polling activity: $e');
|
||||
// Try fallback on error
|
||||
if (_windowManager != null) {
|
||||
print('Attempting fallback polling method...');
|
||||
await _pollActivityHyprlandFallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pollActivityHyprlandFallback() async {
|
||||
try {
|
||||
final result = await Process.run('hyprctl', ['activewindow', '-j']);
|
||||
if (result.exitCode != 0) return;
|
||||
|
||||
@@ -521,10 +590,10 @@ class ProductivityMonitor {
|
||||
_lastActiveWindow = currentApp;
|
||||
_lastActiveWindowTitle = currentWindowTitle;
|
||||
_lastActivityTime = now;
|
||||
print('Switched to: $currentApp');
|
||||
print('Switched to: $currentApp (Hyprland fallback)');
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error polling activity: $e');
|
||||
print('Error in Hyprland fallback polling: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -639,6 +708,9 @@ class ProductivityMonitor {
|
||||
_dbManager.updateDailyStats(xp, focusTime, meetingTime);
|
||||
print('Logged: ${event.type} for ${durationSeconds}s (+$xp XP)');
|
||||
|
||||
// Show cursor overlay for XP gain
|
||||
_cursorOverlayManager?.showXPNumber(xp);
|
||||
|
||||
// Send activity XP notification
|
||||
_xpNotificationManager.showXPGain(
|
||||
source: 'activity',
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import 'dart:io';
|
||||
import '../interfaces/i_window_manager.dart';
|
||||
import '../logging/logger.dart';
|
||||
|
||||
/// Manages cursor-positioned XP overlays by coordinating cursor tracking
|
||||
/// and spawning Flutter overlay processes
|
||||
class CursorOverlayManager {
|
||||
final IWindowManager _windowManager;
|
||||
final String _overlayAppPath;
|
||||
|
||||
CursorOverlayManager({
|
||||
required IWindowManager windowManager,
|
||||
required String overlayAppPath,
|
||||
}) : _windowManager = windowManager,
|
||||
_overlayAppPath = overlayAppPath;
|
||||
|
||||
/// Show XP number at current cursor position
|
||||
///
|
||||
/// [xpValue] - XP amount to display
|
||||
/// [duration] - How long to show the overlay (default 3 seconds)
|
||||
Future<void> showXPNumber(int xpValue, {Duration duration = const Duration(seconds: 3)}) async {
|
||||
try {
|
||||
// Get current cursor position
|
||||
final cursorPos = await _windowManager.getCursorPosition();
|
||||
if (cursorPos == null) {
|
||||
Logger.warn('Could not get cursor position, skipping XP overlay');
|
||||
return;
|
||||
}
|
||||
|
||||
final x = cursorPos['x']!;
|
||||
final y = cursorPos['y']!;
|
||||
|
||||
Logger.info('Showing XP overlay: +$xpValue at position ($x, $y)');
|
||||
|
||||
// Spawn Flutter overlay process
|
||||
await _spawnOverlayProcess(x, y, xpValue, duration);
|
||||
} catch (e) {
|
||||
Logger.error('Failed to show XP overlay', e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Show XP number at specific position
|
||||
///
|
||||
/// [x] - X coordinate in pixels
|
||||
/// [y] - Y coordinate in pixels
|
||||
/// [xpValue] - XP amount to display
|
||||
/// [duration] - How long to show the overlay
|
||||
Future<void> showXPNumberAt(int x, int y, int xpValue, {Duration duration = const Duration(seconds: 3)}) async {
|
||||
try {
|
||||
Logger.info('Showing XP overlay: +$xpValue at position ($x, $y)');
|
||||
await _spawnOverlayProcess(x, y, xpValue, duration);
|
||||
} catch (e) {
|
||||
Logger.error('Failed to show XP overlay at position', e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test cursor position tracking
|
||||
///
|
||||
/// Returns current cursor coordinates or null if unavailable
|
||||
Future<Map<String, int>?> getCurrentCursorPosition() async {
|
||||
try {
|
||||
return await _windowManager.getCursorPosition();
|
||||
} catch (e) {
|
||||
Logger.error('Failed to get cursor position', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn the Flutter overlay process
|
||||
Future<void> _spawnOverlayProcess(int x, int y, int xpValue, Duration duration) async {
|
||||
final durationSeconds = duration.inSeconds;
|
||||
|
||||
// Check if overlay app exists
|
||||
if (!await File(_overlayAppPath).exists()) {
|
||||
Logger.error('Overlay app not found at: $_overlayAppPath');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 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()],
|
||||
mode: ProcessStartMode.detached,
|
||||
);
|
||||
|
||||
Logger.debug('Spawned overlay process with PID: ${process.pid}');
|
||||
|
||||
// Don't wait for the process to complete - let it run independently
|
||||
process.exitCode.then((exitCode) {
|
||||
if (exitCode != 0) {
|
||||
Logger.warn('Overlay process exited with code: $exitCode');
|
||||
} else {
|
||||
Logger.debug('Overlay process completed successfully');
|
||||
}
|
||||
}).catchError((error) {
|
||||
Logger.error('Overlay process error', error);
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
Logger.error('Failed to spawn overlay process', e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test the overlay system by showing a test XP number
|
||||
Future<void> testOverlay({int testXP = 42}) async {
|
||||
Logger.info('Testing overlay system with XP value: $testXP');
|
||||
await showXPNumber(testXP, duration: const Duration(seconds: 5));
|
||||
}
|
||||
|
||||
/// Check if the overlay system is available
|
||||
Future<bool> isAvailable() async {
|
||||
try {
|
||||
// Check if window manager supports cursor position
|
||||
final cursorPos = await _windowManager.getCursorPosition();
|
||||
final windowManagerAvailable = cursorPos != null;
|
||||
|
||||
// Check if overlay app exists
|
||||
final overlayAppAvailable = await File(_overlayAppPath).exists();
|
||||
|
||||
Logger.debug('Overlay system availability: windowManager=$windowManagerAvailable, overlayApp=$overlayAppAvailable');
|
||||
|
||||
return windowManagerAvailable && overlayAppAvailable;
|
||||
} catch (e) {
|
||||
Logger.error('Error checking overlay availability', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import '../detectors/window_manager_detector.dart';
|
||||
|
||||
/// Mock window manager detector for testing
|
||||
///
|
||||
/// Allows tests to override window manager detection behavior
|
||||
class MockWindowManagerDetector {
|
||||
static WindowManager? _mockedWindowManager;
|
||||
static Map<String, String>? _mockedEnvironment;
|
||||
|
||||
/// Set the window manager that should be detected in tests
|
||||
static void mockDetection(WindowManager windowManager) {
|
||||
_mockedWindowManager = windowManager;
|
||||
}
|
||||
|
||||
/// Set mock environment variables for testing detection logic
|
||||
static void mockEnvironment(Map<String, String> environment) {
|
||||
_mockedEnvironment = environment;
|
||||
}
|
||||
|
||||
/// Clear all mocks
|
||||
static void clearMocks() {
|
||||
_mockedWindowManager = null;
|
||||
_mockedEnvironment = null;
|
||||
// Reset the detector singleton
|
||||
WindowManagerDetector.instance.reset();
|
||||
}
|
||||
|
||||
/// Get the currently mocked window manager
|
||||
static WindowManager? get mockedWindowManager => _mockedWindowManager;
|
||||
|
||||
/// Get the currently mocked environment
|
||||
static Map<String, String>? get mockedEnvironment => _mockedEnvironment;
|
||||
|
||||
/// Create a test scenario for Hyprland
|
||||
static void setupHyprlandTest() {
|
||||
mockEnvironment({
|
||||
'HYPRLAND_INSTANCE_SIGNATURE': 'test_signature_12345',
|
||||
'WAYLAND_DISPLAY': 'wayland-1',
|
||||
'XDG_CURRENT_DESKTOP': 'Hyprland',
|
||||
'XDG_SESSION_TYPE': 'wayland',
|
||||
});
|
||||
mockDetection(WindowManager.hyprland);
|
||||
}
|
||||
|
||||
/// Create a test scenario for Sway
|
||||
static void setupSwayTest() {
|
||||
mockEnvironment({
|
||||
'SWAYSOCK': '/run/user/1000/sway-ipc.sock',
|
||||
'WAYLAND_DISPLAY': 'wayland-1',
|
||||
'XDG_CURRENT_DESKTOP': 'sway',
|
||||
'XDG_SESSION_TYPE': 'wayland',
|
||||
});
|
||||
mockDetection(WindowManager.sway);
|
||||
}
|
||||
|
||||
/// Create a test scenario for unknown window manager
|
||||
static void setupUnknownTest() {
|
||||
mockEnvironment({
|
||||
'XDG_SESSION_TYPE': 'x11',
|
||||
'XDG_CURRENT_DESKTOP': 'GNOME',
|
||||
});
|
||||
mockDetection(WindowManager.unknown);
|
||||
}
|
||||
|
||||
/// Verify detection results match expectations
|
||||
static bool verifyDetection(WindowManager expected) {
|
||||
return WindowManagerDetector.instance.detect() == expected;
|
||||
}
|
||||
|
||||
/// Get detailed detection info for debugging tests
|
||||
static Map<String, dynamic> getTestDetectionInfo() {
|
||||
return {
|
||||
'mocked_window_manager': _mockedWindowManager?.toString(),
|
||||
'mocked_environment': _mockedEnvironment,
|
||||
'actual_detection': WindowManagerDetector.instance.detect().toString(),
|
||||
'actual_detection_info': WindowManagerDetector.instance.getDetectionInfo(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import '../interfaces/i_window_manager.dart';
|
||||
|
||||
/// Hyprland window manager implementation
|
||||
class HyprlandManager implements IWindowManager {
|
||||
@override
|
||||
String get managerType => 'Hyprland';
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>?> getCurrentWindow() async {
|
||||
try {
|
||||
final result = await Process.run('hyprctl', ['activewindow', '-j']);
|
||||
if (result.exitCode != 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final windowData = jsonDecode(result.stdout) as Map<String, dynamic>;
|
||||
|
||||
return {
|
||||
'application': windowData['class'] as String? ?? 'unknown',
|
||||
'title': windowData['title'] as String? ?? '',
|
||||
'pid': windowData['pid'] as int? ?? -1,
|
||||
'workspace': windowData['workspace'] != null
|
||||
? (windowData['workspace'] as Map<String, dynamic>)['id'] as int? ?? -1
|
||||
: -1,
|
||||
'address': windowData['address'] as String? ?? '',
|
||||
'floating': windowData['floating'] as bool? ?? false,
|
||||
'fullscreen': windowData['fullscreen'] as bool? ?? false,
|
||||
'size': windowData['size'] != null
|
||||
? [
|
||||
(windowData['size'] as List<dynamic>)[0] as int,
|
||||
(windowData['size'] as List<dynamic>)[1] as int,
|
||||
]
|
||||
: [0, 0],
|
||||
'position': windowData['at'] != null
|
||||
? [
|
||||
(windowData['at'] as List<dynamic>)[0] as int,
|
||||
(windowData['at'] as List<dynamic>)[1] as int,
|
||||
]
|
||||
: [0, 0],
|
||||
};
|
||||
} catch (e) {
|
||||
print('Error getting current window from Hyprland: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getActiveApplication() async {
|
||||
final window = await getCurrentWindow();
|
||||
return window?['application'] as String? ?? 'unknown';
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getWindowTitle() async {
|
||||
final window = await getCurrentWindow();
|
||||
return window?['title'] as String? ?? '';
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>?> getCurrentWorkspace() async {
|
||||
try {
|
||||
final result = await Process.run('hyprctl', ['activeworkspace', '-j']);
|
||||
if (result.exitCode != 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final workspaceData = jsonDecode(result.stdout) as Map<String, dynamic>;
|
||||
|
||||
return {
|
||||
'id': workspaceData['id'] as int? ?? -1,
|
||||
'name': workspaceData['name'] as String? ?? '',
|
||||
'monitor': workspaceData['monitor'] as String? ?? '',
|
||||
'windows': workspaceData['windows'] as int? ?? 0,
|
||||
'hasfullscreen': workspaceData['hasfullscreen'] as bool? ?? false,
|
||||
'lastwindow': workspaceData['lastwindow'] as String? ?? '',
|
||||
'lastwindowtitle': workspaceData['lastwindowtitle'] as String? ?? '',
|
||||
};
|
||||
} catch (e) {
|
||||
print('Error getting current workspace from Hyprland: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> isAvailable() async {
|
||||
try {
|
||||
final result = await Process.run('hyprctl', ['version']);
|
||||
return result.exitCode == 0;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> getVersion() async {
|
||||
try {
|
||||
final result = await Process.run('hyprctl', ['version']);
|
||||
if (result.exitCode == 0) {
|
||||
final output = result.stdout as String;
|
||||
final lines = output.split('\n');
|
||||
if (lines.isNotEmpty) {
|
||||
return lines.first.trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all windows from Hyprland
|
||||
Future<List<Map<String, dynamic>>> getAllWindows() async {
|
||||
try {
|
||||
final result = await Process.run('hyprctl', ['clients', '-j']);
|
||||
if (result.exitCode != 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final clientsData = jsonDecode(result.stdout) as List<dynamic>;
|
||||
return clientsData.cast<Map<String, dynamic>>();
|
||||
} catch (e) {
|
||||
print('Error getting all windows from Hyprland: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all workspaces from Hyprland
|
||||
Future<List<Map<String, dynamic>>> getAllWorkspaces() async {
|
||||
try {
|
||||
final result = await Process.run('hyprctl', ['workspaces', '-j']);
|
||||
if (result.exitCode != 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final workspacesData = jsonDecode(result.stdout) as List<dynamic>;
|
||||
return workspacesData.cast<Map<String, dynamic>>();
|
||||
} catch (e) {
|
||||
print('Error getting all workspaces from Hyprland: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Get monitor information from Hyprland
|
||||
Future<List<Map<String, dynamic>>> getMonitors() async {
|
||||
try {
|
||||
final result = await Process.run('hyprctl', ['monitors', '-j']);
|
||||
if (result.exitCode != 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final monitorsData = jsonDecode(result.stdout) as List<dynamic>;
|
||||
return monitorsData.cast<Map<String, dynamic>>();
|
||||
} catch (e) {
|
||||
print('Error getting monitors from Hyprland: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a Hyprland dispatch command
|
||||
Future<bool> dispatch(String command, [List<String> args = const []]) async {
|
||||
try {
|
||||
final fullArgs = ['dispatch', command, ...args];
|
||||
final result = await Process.run('hyprctl', fullArgs);
|
||||
return result.exitCode == 0;
|
||||
} catch (e) {
|
||||
print('Error executing Hyprland dispatch: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, int>?> getCursorPosition() async {
|
||||
try {
|
||||
final result = await Process.run('hyprctl', ['cursorpos']);
|
||||
if (result.exitCode != 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final output = result.stdout as String;
|
||||
// Parse output format: "123, 456" (x, y coordinates)
|
||||
final coordinates = output.trim().split(', ');
|
||||
if (coordinates.length != 2) {
|
||||
print('Unexpected cursor position format: $output');
|
||||
return null;
|
||||
}
|
||||
|
||||
final x = int.tryParse(coordinates[0]);
|
||||
final y = int.tryParse(coordinates[1]);
|
||||
|
||||
if (x == null || y == null) {
|
||||
print('Failed to parse cursor coordinates: $output');
|
||||
return null;
|
||||
}
|
||||
|
||||
return {'x': x, 'y': y};
|
||||
} catch (e) {
|
||||
print('Error getting cursor position from Hyprland: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import '../interfaces/i_window_manager.dart';
|
||||
|
||||
/// Sway window manager implementation
|
||||
class SwayManager implements IWindowManager {
|
||||
@override
|
||||
String get managerType => 'Sway';
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>?> getCurrentWindow() async {
|
||||
try {
|
||||
final result = await Process.run('swaymsg', ['-t', 'get_tree']);
|
||||
if (result.exitCode != 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final treeData = jsonDecode(result.stdout) as Map<String, dynamic>;
|
||||
final focusedNode = _findFocusedNode(treeData);
|
||||
|
||||
if (focusedNode == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
'application': _extractAppId(focusedNode),
|
||||
'title': _extractWindowTitle(focusedNode),
|
||||
'pid': focusedNode['pid'] as int? ?? -1,
|
||||
'workspace': _extractWorkspaceNumber(focusedNode),
|
||||
'id': focusedNode['id'] as int? ?? -1,
|
||||
'floating': _isFloating(focusedNode),
|
||||
'fullscreen_mode': focusedNode['fullscreen_mode'] as int? ?? 0,
|
||||
'rect': focusedNode['rect'] != null
|
||||
? Map<String, dynamic>.from(focusedNode['rect'] as Map)
|
||||
: null,
|
||||
'window_rect': focusedNode['window_rect'] != null
|
||||
? Map<String, dynamic>.from(focusedNode['window_rect'] as Map)
|
||||
: null,
|
||||
'type': focusedNode['type'] as String? ?? 'unknown',
|
||||
};
|
||||
} catch (e) {
|
||||
print('Error getting current window from Sway: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getActiveApplication() async {
|
||||
final window = await getCurrentWindow();
|
||||
return window?['application'] as String? ?? 'unknown';
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getWindowTitle() async {
|
||||
final window = await getCurrentWindow();
|
||||
return window?['title'] as String? ?? '';
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>?> getCurrentWorkspace() async {
|
||||
try {
|
||||
final result = await Process.run('swaymsg', ['-t', 'get_workspaces']);
|
||||
if (result.exitCode != 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final workspacesData = jsonDecode(result.stdout) as List<dynamic>;
|
||||
|
||||
// Find the focused workspace
|
||||
for (final workspace in workspacesData) {
|
||||
if (workspace is Map<String, dynamic> && workspace['focused'] == true) {
|
||||
return {
|
||||
'id': workspace['num'] as int? ?? -1,
|
||||
'name': workspace['name'] as String? ?? '',
|
||||
'output': workspace['output'] as String? ?? '',
|
||||
'focused': workspace['focused'] as bool? ?? false,
|
||||
'urgent': workspace['urgent'] as bool? ?? false,
|
||||
'visible': workspace['visible'] as bool? ?? false,
|
||||
'rect': workspace['rect'] != null
|
||||
? Map<String, dynamic>.from(workspace['rect'] as Map)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
print('Error getting current workspace from Sway: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> isAvailable() async {
|
||||
try {
|
||||
final result = await Process.run('swaymsg', ['-t', 'get_version']);
|
||||
return result.exitCode == 0;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> getVersion() async {
|
||||
try {
|
||||
final result = await Process.run('swaymsg', ['-t', 'get_version']);
|
||||
if (result.exitCode == 0) {
|
||||
final versionData = jsonDecode(result.stdout) as Map<String, dynamic>;
|
||||
return versionData['human_readable'] as String? ?? 'Sway (version unknown)';
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively find the focused node in the Sway tree
|
||||
Map<String, dynamic>? _findFocusedNode(Map<String, dynamic> node) {
|
||||
// Check if this node is focused
|
||||
if (node['focused'] == true) {
|
||||
return node;
|
||||
}
|
||||
|
||||
// Check child nodes
|
||||
final nodes = node['nodes'] as List<dynamic>? ?? [];
|
||||
for (final childNode in nodes) {
|
||||
if (childNode is Map<String, dynamic>) {
|
||||
final focused = _findFocusedNode(childNode);
|
||||
if (focused != null) {
|
||||
return focused;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check floating nodes
|
||||
final floatingNodes = node['floating_nodes'] as List<dynamic>? ?? [];
|
||||
for (final floatingNode in floatingNodes) {
|
||||
if (floatingNode is Map<String, dynamic>) {
|
||||
final focused = _findFocusedNode(floatingNode);
|
||||
if (focused != null) {
|
||||
return focused;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Extract application ID from a Sway node
|
||||
String _extractAppId(Map<String, dynamic> node) {
|
||||
// For Wayland applications, use app_id
|
||||
final appId = node['app_id'] as String?;
|
||||
if (appId != null && appId.isNotEmpty) {
|
||||
return appId;
|
||||
}
|
||||
|
||||
// For X11 applications, use window_properties.class
|
||||
final windowProperties = node['window_properties'] as Map<String, dynamic>?;
|
||||
if (windowProperties != null) {
|
||||
final windowClass = windowProperties['class'] as String?;
|
||||
if (windowClass != null && windowClass.isNotEmpty) {
|
||||
return windowClass;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to window type or node type
|
||||
final type = node['type'] as String?;
|
||||
if (type != null && type != 'con') {
|
||||
return type;
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
/// Extract window title from a Sway node
|
||||
String _extractWindowTitle(Map<String, dynamic> node) {
|
||||
// Try the name field first
|
||||
final name = node['name'] as String?;
|
||||
if (name != null && name.isNotEmpty) {
|
||||
return name;
|
||||
}
|
||||
|
||||
// For X11 applications, try window_properties.title
|
||||
final windowProperties = node['window_properties'] as Map<String, dynamic>?;
|
||||
if (windowProperties != null) {
|
||||
final title = windowProperties['title'] as String?;
|
||||
if (title != null && title.isNotEmpty) {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/// Extract workspace number from a Sway node by traversing up the tree
|
||||
int _extractWorkspaceNumber(Map<String, dynamic> node) {
|
||||
// If this node has a workspace number, return it
|
||||
final num = node['num'] as int?;
|
||||
if (num != null && num > 0) {
|
||||
return num;
|
||||
}
|
||||
|
||||
// This is a simplified approach - in practice, you might need to
|
||||
// maintain the parent relationship while traversing the tree
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// Check if a node represents a floating window
|
||||
bool _isFloating(Map<String, dynamic> node) {
|
||||
final type = node['type'] as String?;
|
||||
return type == 'floating_con';
|
||||
}
|
||||
|
||||
/// Get all workspaces from Sway
|
||||
Future<List<Map<String, dynamic>>> getAllWorkspaces() async {
|
||||
try {
|
||||
final result = await Process.run('swaymsg', ['-t', 'get_workspaces']);
|
||||
if (result.exitCode != 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final workspacesData = jsonDecode(result.stdout) as List<dynamic>;
|
||||
return workspacesData.cast<Map<String, dynamic>>();
|
||||
} catch (e) {
|
||||
print('Error getting all workspaces from Sway: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Get outputs (monitors) from Sway
|
||||
Future<List<Map<String, dynamic>>> getOutputs() async {
|
||||
try {
|
||||
final result = await Process.run('swaymsg', ['-t', 'get_outputs']);
|
||||
if (result.exitCode != 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final outputsData = jsonDecode(result.stdout) as List<dynamic>;
|
||||
return outputsData.cast<Map<String, dynamic>>();
|
||||
} catch (e) {
|
||||
print('Error getting outputs from Sway: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the full window tree from Sway
|
||||
Future<Map<String, dynamic>?> getWindowTree() async {
|
||||
try {
|
||||
final result = await Process.run('swaymsg', ['-t', 'get_tree']);
|
||||
if (result.exitCode != 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return jsonDecode(result.stdout) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
print('Error getting window tree from Sway: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a Sway command
|
||||
Future<bool> executeCommand(String command) async {
|
||||
try {
|
||||
final result = await Process.run('swaymsg', [command]);
|
||||
return result.exitCode == 0;
|
||||
} catch (e) {
|
||||
print('Error executing Sway command: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, int>?> getCursorPosition() async {
|
||||
try {
|
||||
// Sway doesn't have a direct cursor position query command
|
||||
// As a fallback, we'll return the center of the focused window
|
||||
final window = await getCurrentWindow();
|
||||
if (window == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final rect = window['rect'] as Map<String, dynamic>?;
|
||||
if (rect == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final x = (rect['x'] as int? ?? 0) + ((rect['width'] as int? ?? 0) ~/ 2);
|
||||
final y = (rect['y'] as int? ?? 0) + ((rect['height'] as int? ?? 0) ~/ 2);
|
||||
|
||||
return {'x': x, 'y': y};
|
||||
} catch (e) {
|
||||
print('Error getting cursor position from Sway: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user