[AI] fix
This commit is contained in:
89
.gitignore
vendored
Normal file
89
.gitignore
vendored
Normal file
@@ -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
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
"""Time Tracker package."""
|
"""Entry point for running the time tracker as a module."""
|
||||||
|
|
||||||
from .main import main
|
from .main import main
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
@@ -51,16 +51,28 @@ except ImportError:
|
|||||||
return 0
|
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:
|
class WorkTracker:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.config = load_config()
|
raw_config = load_config()
|
||||||
|
self.config = ConfigWrapper(raw_config)
|
||||||
self.session_manager = SessionManager("tasks")
|
self.session_manager = SessionManager("tasks")
|
||||||
self.active_window = ""
|
self.active_window = ""
|
||||||
self.window_classification = "neutral"
|
self.window_classification = "neutral"
|
||||||
self.last_activity = time.time()
|
self.last_activity = time.time()
|
||||||
self.session_locked = False
|
self.session_locked = False
|
||||||
self.is_tracking = self.config.get("tracking_enabled", True)
|
self.is_tracking = self.config.tracking_enabled
|
||||||
self.current_task_id = self.config.get("current_task_id")
|
self.current_task_id = self.config.current_task_id
|
||||||
|
|
||||||
# Start monitoring in background
|
# Start monitoring in background
|
||||||
self.start_monitoring()
|
self.start_monitoring()
|
||||||
@@ -119,7 +131,7 @@ class WorkTracker:
|
|||||||
def classify_window(self, title: str) -> str:
|
def classify_window(self, title: str) -> str:
|
||||||
"""Classify window as work, distraction, or neutral"""
|
"""Classify window as work, distraction, or neutral"""
|
||||||
title_lower = title.lower()
|
title_lower = title.lower()
|
||||||
rules = self.config.get("window_rules", {})
|
rules = self.config.window_rules # Access through wrapper
|
||||||
|
|
||||||
# Check distractions first
|
# Check distractions first
|
||||||
for keyword in rules.get("distraction", []):
|
for keyword in rules.get("distraction", []):
|
||||||
@@ -150,7 +162,6 @@ class WorkTracker:
|
|||||||
# Check if tracking is enabled
|
# Check if tracking is enabled
|
||||||
if not self.is_tracking:
|
if not self.is_tracking:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Check if session is locked
|
# Check if session is locked
|
||||||
if self.session_locked:
|
if self.session_locked:
|
||||||
return False
|
return False
|
||||||
@@ -186,16 +197,19 @@ class WorkTracker:
|
|||||||
def toggle_tracking(self):
|
def toggle_tracking(self):
|
||||||
"""Toggle tracking on/off"""
|
"""Toggle tracking on/off"""
|
||||||
self.is_tracking = not self.is_tracking
|
self.is_tracking = not self.is_tracking
|
||||||
self.config["tracking_enabled"] = self.is_tracking
|
self.config.tracking_enabled = self.is_tracking
|
||||||
save_config(self.config)
|
# 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):
|
def create_task(self, task_id: str):
|
||||||
"""Create a new task and start tracking it"""
|
"""Create a new task and start tracking it"""
|
||||||
project_id = task_id.split('-')[0] if '-' in task_id else task_id
|
project_id = task_id.split('-')[0] if '-' in task_id else task_id
|
||||||
self.session_manager.create_session(task_id, project_id)
|
self.session_manager.create_session(task_id, project_id)
|
||||||
self.current_task_id = task_id
|
self.current_task_id = task_id
|
||||||
self.config["current_task_id"] = task_id
|
self.config.current_task_id = task_id
|
||||||
save_config(self.config)
|
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):
|
def switch_task(self, task_id: str):
|
||||||
"""Switch to an existing task"""
|
"""Switch to an existing task"""
|
||||||
@@ -207,8 +221,9 @@ class WorkTracker:
|
|||||||
project_id = task_id.split('-')[0] if '-' in task_id else task_id
|
project_id = task_id.split('-')[0] if '-' in task_id else task_id
|
||||||
self.session_manager.create_session(task_id, project_id)
|
self.session_manager.create_session(task_id, project_id)
|
||||||
self.current_task_id = task_id
|
self.current_task_id = task_id
|
||||||
self.config["current_task_id"] = task_id
|
self.config.current_task_id = task_id
|
||||||
save_config(self.config)
|
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]:
|
def get_available_tasks(self) -> List[str]:
|
||||||
"""Get list of all available tasks"""
|
"""Get list of all available tasks"""
|
||||||
|
|||||||
@@ -18,11 +18,26 @@ def register_session_listener(callback: Callable[[str], None]) -> None:
|
|||||||
"""Register a callback for Windows session lock/unlock events"""
|
"""Register a callback for Windows session lock/unlock events"""
|
||||||
|
|
||||||
def session_event_handler(hwnd, msg, wparam, lparam):
|
def session_event_handler(hwnd, msg, wparam, lparam):
|
||||||
if msg == win32con.WM_WTSSESSION_CHANGE:
|
# Check if the constant exists before using it
|
||||||
if wparam == win32con.WTS_SESSION_LOCK:
|
try:
|
||||||
callback("locked")
|
if msg == win32con.WM_WTSSESSION_CHANGE:
|
||||||
elif wparam == win32con.WTS_SESSION_UNLOCK:
|
if wparam == getattr(win32con, 'WTS_SESSION_LOCK', 0x1):
|
||||||
callback("unlocked")
|
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)
|
return win32gui.DefWindowProc(hwnd, msg, wparam, lparam)
|
||||||
|
|
||||||
def run_message_loop():
|
def run_message_loop():
|
||||||
@@ -39,17 +54,17 @@ def register_session_listener(callback: Callable[[str], None]) -> None:
|
|||||||
|
|
||||||
# Corrected CreateWindow call with all required parameters
|
# Corrected CreateWindow call with all required parameters
|
||||||
hwnd = win32gui.CreateWindow(
|
hwnd = win32gui.CreateWindow(
|
||||||
wndclass.lpszClassName, # lpClassName
|
wndclass.lpszClassName, # lpClassName
|
||||||
"Session Watcher", # lpWindowName
|
"Session Watcher", # lpWindowName
|
||||||
0, # dwStyle
|
0, # dwStyle
|
||||||
0, # x
|
0, # x
|
||||||
0, # y
|
0, # y
|
||||||
0, # nWidth
|
0, # nWidth
|
||||||
0, # nHeight
|
0, # nHeight
|
||||||
0, # hWndParent
|
0, # hWndParent
|
||||||
0, # hMenu
|
0, # hMenu
|
||||||
hinst, # hInstance
|
hinst, # hInstance
|
||||||
None # lParam
|
None # lParam
|
||||||
)
|
)
|
||||||
|
|
||||||
# Register for session notifications
|
# Register for session notifications
|
||||||
|
|||||||
133
tests/test_main_integration.py
Normal file
133
tests/test_main_integration.py
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user