From 01cf0f87d0de7d2265f46cf234da5ef343a35639 Mon Sep 17 00:00:00 2001 From: Mikan <72257910+Mikan-DS@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:05:48 +0300 Subject: [PATCH] [AI] fix --- .gitignore | 89 ++++++++++++++++++ src/time_tracker/__main__.py | 4 +- src/time_tracker/main.py | 37 +++++--- src/time_tracker/platform/windows.py | 47 ++++++---- tests/test_main_integration.py | 133 +++++++++++++++++++++++++++ 5 files changed, 282 insertions(+), 28 deletions(-) create mode 100644 .gitignore create mode 100644 tests/test_main_integration.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1472ae5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,89 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# Pyre type checker +.pyre/ + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db +Icon? + +# Configuration and data files generated by the application +config.json +tasks/ +*.json \ No newline at end of file diff --git a/src/time_tracker/__main__.py b/src/time_tracker/__main__.py index 64ac5c2..6c2a1da 100644 --- a/src/time_tracker/__main__.py +++ b/src/time_tracker/__main__.py @@ -1,4 +1,6 @@ -"""Time Tracker package.""" +"""Entry point for running the time tracker as a module.""" + from .main import main + if __name__ == "__main__": main() \ No newline at end of file diff --git a/src/time_tracker/main.py b/src/time_tracker/main.py index f3bd17b..1a99a20 100644 --- a/src/time_tracker/main.py +++ b/src/time_tracker/main.py @@ -51,16 +51,28 @@ except ImportError: return 0 +class ConfigWrapper: + """Wrapper to allow attribute access to config dict""" + + def __init__(self, config_dict: Dict): + self.__dict__.update(config_dict) + + def update(self, new_config: Dict): + """Update the config and underlying dict""" + self.__dict__.update(new_config) + + class WorkTracker: def __init__(self): - self.config = load_config() + raw_config = load_config() + self.config = ConfigWrapper(raw_config) self.session_manager = SessionManager("tasks") self.active_window = "" self.window_classification = "neutral" self.last_activity = time.time() self.session_locked = False - self.is_tracking = self.config.get("tracking_enabled", True) - self.current_task_id = self.config.get("current_task_id") + self.is_tracking = self.config.tracking_enabled + self.current_task_id = self.config.current_task_id # Start monitoring in background self.start_monitoring() @@ -119,7 +131,7 @@ class WorkTracker: def classify_window(self, title: str) -> str: """Classify window as work, distraction, or neutral""" title_lower = title.lower() - rules = self.config.get("window_rules", {}) + rules = self.config.window_rules # Access through wrapper # Check distractions first for keyword in rules.get("distraction", []): @@ -150,7 +162,6 @@ class WorkTracker: # Check if tracking is enabled if not self.is_tracking: return False - # Check if session is locked if self.session_locked: return False @@ -186,16 +197,19 @@ class WorkTracker: def toggle_tracking(self): """Toggle tracking on/off""" self.is_tracking = not self.is_tracking - self.config["tracking_enabled"] = self.is_tracking - save_config(self.config) + self.config.tracking_enabled = self.is_tracking + # Update raw config too + raw_config = {key: value for key, value in vars(self.config).items() if not key.startswith('_')} + save_config(raw_config) def create_task(self, task_id: str): """Create a new task and start tracking it""" project_id = task_id.split('-')[0] if '-' in task_id else task_id self.session_manager.create_session(task_id, project_id) self.current_task_id = task_id - self.config["current_task_id"] = task_id - save_config(self.config) + self.config.current_task_id = task_id + raw_config = {key: value for key, value in vars(self.config).items() if not key.startswith('_')} + save_config(raw_config) def switch_task(self, task_id: str): """Switch to an existing task""" @@ -207,8 +221,9 @@ class WorkTracker: project_id = task_id.split('-')[0] if '-' in task_id else task_id self.session_manager.create_session(task_id, project_id) self.current_task_id = task_id - self.config["current_task_id"] = task_id - save_config(self.config) + self.config.current_task_id = task_id + raw_config = {key: value for key, value in vars(self.config).items() if not key.startswith('_')} + save_config(raw_config) def get_available_tasks(self) -> List[str]: """Get list of all available tasks""" diff --git a/src/time_tracker/platform/windows.py b/src/time_tracker/platform/windows.py index e228b96..c571e0d 100644 --- a/src/time_tracker/platform/windows.py +++ b/src/time_tracker/platform/windows.py @@ -18,11 +18,26 @@ def register_session_listener(callback: Callable[[str], None]) -> None: """Register a callback for Windows session lock/unlock events""" def session_event_handler(hwnd, msg, wparam, lparam): - if msg == win32con.WM_WTSSESSION_CHANGE: - if wparam == win32con.WTS_SESSION_LOCK: - callback("locked") - elif wparam == win32con.WTS_SESSION_UNLOCK: - callback("unlocked") + # Check if the constant exists before using it + try: + if msg == win32con.WM_WTSSESSION_CHANGE: + if wparam == getattr(win32con, 'WTS_SESSION_LOCK', 0x1): + callback("locked") + elif wparam == getattr(win32con, 'WTS_SESSION_UNLOCK', 0x2): + callback("unlocked") + except AttributeError: + # Some versions of pywin32 might not have these constants + # Use numeric values as fallback + WM_WTSSESSION_CHANGE = 0x02B1 + WTS_SESSION_LOCK = 0x1 + WTS_SESSION_UNLOCK = 0x2 + + if msg == WM_WTSSESSION_CHANGE: + if wparam == WTS_SESSION_LOCK: + callback("locked") + elif wparam == WTS_SESSION_UNLOCK: + callback("unlocked") + return win32gui.DefWindowProc(hwnd, msg, wparam, lparam) def run_message_loop(): @@ -39,17 +54,17 @@ def register_session_listener(callback: Callable[[str], None]) -> None: # Corrected CreateWindow call with all required parameters hwnd = win32gui.CreateWindow( - wndclass.lpszClassName, # lpClassName - "Session Watcher", # lpWindowName - 0, # dwStyle - 0, # x - 0, # y - 0, # nWidth - 0, # nHeight - 0, # hWndParent - 0, # hMenu - hinst, # hInstance - None # lParam + wndclass.lpszClassName, # lpClassName + "Session Watcher", # lpWindowName + 0, # dwStyle + 0, # x + 0, # y + 0, # nWidth + 0, # nHeight + 0, # hWndParent + 0, # hMenu + hinst, # hInstance + None # lParam ) # Register for session notifications diff --git a/tests/test_main_integration.py b/tests/test_main_integration.py new file mode 100644 index 0000000..f39ae9d --- /dev/null +++ b/tests/test_main_integration.py @@ -0,0 +1,133 @@ +import json +import os +import tempfile +from unittest.mock import patch, MagicMock +import tkinter as tk +from time_tracker.main import WorkTracker, ConfigWrapper + + +def test_tracker_initialization(): + """Test that WorkTracker initializes without errors""" + # Create a temporary config file to avoid creating one in the current directory + with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as tmp: + json.dump({ + "tracking_enabled": True, + "current_task_id": None, + "window_rules": { + "work": ["PyCharm", "VS Code"], + "distraction": ["YouTube", "Twitter"] + } + }, tmp) + tmp_path = tmp.name + + original_config_path = "config.json" + + # Temporarily replace the config file + if os.path.exists(original_config_path): + os.rename(original_config_path, f"{original_config_path}.backup") + + os.rename(tmp_path, original_config_path) + + try: + # Initialize the tracker + tracker = WorkTracker() + + # Verify that config is properly loaded as a ConfigWrapper object + assert hasattr(tracker, 'config') + assert isinstance(tracker.config, ConfigWrapper) + assert hasattr(tracker.config, 'tracking_enabled') + assert hasattr(tracker.config, 'current_task_id') + assert hasattr(tracker.config, 'window_rules') + assert tracker.config.tracking_enabled is True + assert tracker.config.current_task_id is None + + finally: + # Restore original config file + os.remove(original_config_path) + if os.path.exists(f"{original_config_path}.backup"): + os.rename(f"{original_config_path}.backup", original_config_path) + + +def test_config_wrapper_functionality(): + """Test that ConfigWrapper provides attribute access to config values""" + config_dict = { + "tracking_enabled": True, + "current_task_id": "TEST-123", + "window_rules": { + "work": ["PyCharm", "VS Code"], + "distraction": ["YouTube", "Twitter"] + } + } + + wrapper = ConfigWrapper(config_dict) + + # Test attribute access + assert wrapper.tracking_enabled is True + assert wrapper.current_task_id == "TEST-123" + assert "PyCharm" in wrapper.window_rules["work"] + + # Test updating config + wrapper.tracking_enabled = False + assert wrapper.tracking_enabled is False + # Note: Direct assignment doesn't update the original dict, but the wrapper still works + + +def test_tracker_config_attribute_access(): + """Test that config can be accessed as an object with attributes""" + + # Create a mock config object that behaves like the real config + class MockConfig: + def __init__(self): + self.tracking_enabled = True + self.current_task_id = "TEST-123" + + config = MockConfig() + assert hasattr(config, 'tracking_enabled') + assert config.tracking_enabled is True + assert config.current_task_id == "TEST-123" + + +def test_overlay_compatible_config(): + """Test that the config structure is compatible with overlay expectations""" + from time_tracker.core.config import load_config + + # Create a temporary config file + with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as tmp: + json.dump({ + "tracking_enabled": True, + "current_task_id": "TEST-123", + "window_rules": { + "work": ["PyCharm", "VS Code"], + "distraction": ["YouTube", "Twitter"] + } + }, tmp) + tmp_path = tmp.name + + original_config_path = "config.json" + + # Temporarily replace the config file + if os.path.exists(original_config_path): + os.rename(original_config_path, f"{original_config_path}.backup") + + os.rename(tmp_path, original_config_path) + + try: + # Load config using the actual function + config = load_config() + + # Verify structure + assert isinstance(config, dict) + assert config.get("tracking_enabled") is True + assert config.get("current_task_id") == "TEST-123" + + # Create a compatible object that supports attribute access + wrapped_config = ConfigWrapper(config) + assert hasattr(wrapped_config, 'tracking_enabled') + assert wrapped_config.tracking_enabled is True + assert wrapped_config.current_task_id == "TEST-123" + + finally: + # Restore original config file + os.remove(original_config_path) + if os.path.exists(f"{original_config_path}.backup"): + os.rename(f"{original_config_path}.backup", original_config_path) \ No newline at end of file