diff --git a/src/time_tracker/main.py b/src/time_tracker/main.py index 86cba80..7760708 100644 --- a/src/time_tracker/main.py +++ b/src/time_tracker/main.py @@ -11,54 +11,38 @@ try: from .core.session_manager import SessionManager from .ui.overlay import OverlayWindow - # Try to import platform-specific modules import sys - if sys.platform.startswith('win'): from .platform.windows import get_active_window, register_session_listener, get_idle_time else: - # Fallback for other platforms def get_active_window(): return "Unsupported platform" - - def register_session_listener(callback): - print("Session monitoring not supported on this platform") - - + print("Session monitoring not supported") def get_idle_time(): return 0 except ImportError: - # For running as a single file from core.config import load_config, save_config from core.session_manager import SessionManager from ui.overlay import OverlayWindow import sys - if sys.platform.startswith('win'): from platform.windows import get_active_window, register_session_listener, get_idle_time else: def get_active_window(): return "Unsupported platform" - - def register_session_listener(callback): - print("Session monitoring not supported on this platform") - - + print("Session monitoring not supported") def get_idle_time(): 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) @@ -74,261 +58,212 @@ class WorkTracker: self.is_tracking = self.config.tracking_enabled self.current_task_id = self.config.current_task_id - # Start monitoring in background + # Thread safety + self._state_lock = threading.Lock() + self.start_monitoring() def start_monitoring(self): - """Start background monitoring threads""" - # Thread for window tracking window_thread = threading.Thread(target=self.monitor_windows, daemon=True) window_thread.start() - # Thread for input activity tracking input_thread = threading.Thread(target=self.monitor_input_activity, daemon=True) input_thread.start() - # Register session listener register_session_listener(self.on_session_change) def monitor_windows(self): - """Monitor active windows and update classifications""" while True: try: current_window = get_active_window() - - # Only update if window changed if current_window != self.active_window: - self.active_window = current_window - self.window_classification = self.classify_window(current_window) - - # Show notification if it's a distraction or neutral app - if self.window_classification == "distraction": - self.show_distraction_notification(current_window) - elif self.window_classification == "neutral": - self.show_neutral_notification(current_window) + with self._state_lock: + self.active_window = current_window + self.window_classification = self.classify_window(current_window) + # Notifications will be shown via callbacks set in main() time.sleep(1) except Exception as e: print(f"Error in window monitoring: {e}") time.sleep(5) def monitor_input_activity(self): - """Monitor keyboard and mouse activity""" try: from pynput import mouse, keyboard def on_activity(*args): self.last_activity = time.time() - # Start listeners mouse.Listener(on_move=on_activity, on_click=on_activity).start() keyboard.Listener(on_press=on_activity).start() except ImportError: print("pynput not available, using fallback idle detection") - # Fallback: just use system idle time - pass + # Fallback: periodically check system idle time + def idle_fallback(): + while True: + try: + idle_sec = get_idle_time() + if idle_sec < 5: # Consider active if idle < 5 sec + self.last_activity = time.time() + except: + pass + time.sleep(2) + threading.Thread(target=idle_fallback, daemon=True).start() def classify_window(self, title: str) -> str: - """Classify window as work, distraction, or neutral""" title_lower = title.lower() - rules = self.config.window_rules # Access through wrapper + rules = self.config.window_rules - # Check distractions first for keyword in rules.get("distraction", []): if keyword.lower() in title_lower: return "distraction" - # Then check work apps for keyword in rules.get("work", []): if keyword.lower() in title_lower: return "work" - # Otherwise neutral return "neutral" def on_session_change(self, event_type: str): - """Handle session lock/unlock events""" if event_type == "locked": self.session_locked = True - # End current session if active if self.session_manager.current_session: self.session_manager.end_current_session() elif event_type == "unlocked": self.session_locked = False - self.last_activity = time.time() # Reset idle time on unlock + self.last_activity = time.time() def should_count_as_work(self) -> bool: - """Check if current conditions should count as work time""" - # Check if tracking is enabled - if not self.is_tracking: - return False - - # Check if session is locked - if self.session_locked: - return False - - # Check idle time (more than 60 seconds of inactivity) - idle_time = time.time() - self.last_activity - if idle_time > 60: - return False - - # Check window classification - if self.window_classification != "work": - return False - - return True + with self._state_lock: + if not self.is_tracking: + return False + if self.session_locked: + return False + idle_time = time.time() - self.last_activity + if idle_time > 60: + return False + return self.window_classification == "work" def run_main_loop(self): - """Main tracking loop""" while True: try: if self.should_count_as_work(): - # Increment current session duration if self.session_manager.current_session: self.session_manager.increment_duration(1) - - # Record time for current window self.session_manager.record_window_time(self.active_window, 1) - time.sleep(1) except Exception as e: print(f"Error in main loop: {e}") time.sleep(5) def toggle_tracking(self): - """Toggle tracking on/off""" - self.is_tracking = not self.is_tracking - 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) + with self._state_lock: + self.is_tracking = not self.is_tracking + self.config.tracking_enabled = self.is_tracking + raw_config = {k: v for k, v in vars(self.config).items() if not k.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 - raw_config = {key: value for key, value in vars(self.config).items() if not key.startswith('_')} + with self._state_lock: + self.current_task_id = task_id + self.config.current_task_id = task_id + raw_config = {k: v for k, v in vars(self.config).items() if not k.startswith('_')} save_config(raw_config) def switch_task(self, task_id: str): - """Switch to an existing task""" - # End current session if exists if self.session_manager.current_session: self.session_manager.end_current_session() - # Start new session for the task 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 - raw_config = {key: value for key, value in vars(self.config).items() if not key.startswith('_')} + with self._state_lock: + self.current_task_id = task_id + self.config.current_task_id = task_id + raw_config = {k: v for k, v in vars(self.config).items() if not k.startswith('_')} save_config(raw_config) def get_available_tasks(self) -> List[str]: - """Get list of all available tasks""" tasks_dir = "tasks" if not os.path.exists(tasks_dir): return [] - - tasks = [] - for item in os.listdir(tasks_dir): - item_path = os.path.join(tasks_dir, item) - if os.path.isdir(item_path): - tasks.append(item) - - return tasks + return [item for item in os.listdir(tasks_dir) if os.path.isdir(os.path.join(tasks_dir, item))] def has_active_session(self) -> bool: - """Check if there's an active session""" return self.session_manager.current_session is not None def add_manual_adjustment(self, seconds: int, reason: str): - """Add a manual time adjustment""" if self.session_manager.current_session: self.session_manager.add_manual_adjustment(seconds, reason) def get_current_session_duration(self) -> int: - """Get duration of current session""" return self.session_manager.get_current_session_duration() def get_total_work_today(self) -> int: - """Get total work time for today""" return self.session_manager.get_total_work_today() def get_planned_duration(self, task_id: str) -> Optional[int]: - """Get planned duration for a task (placeholder implementation)""" - # In a full implementation, this would look up the planned time for the task - # For now, return None to indicate no planned time return None - def show_distraction_notification(self, window_title: str): - """Show notification about distraction window""" - # In a full implementation, this would show a GUI notification - print(f"DISTRACTION: {window_title}") - - def show_neutral_notification(self, window_title: str): - """Show notification about neutral window""" - # In a full implementation, this would show a GUI notification - print(f"NEUTRAL APP: {window_title}") + # Notification methods will be replaced by callbacks in main() -def main(): - # Create main Tkinter root - root = tk.Tk() - root.title("Work Tracker") # Give it a title so it appears in taskbar - root.geometry("1x1+2000+2000") # Minimal size off-screen to avoid flicker - # Don't call withdraw() so the window manager recognizes it - - # Initialize tracker - tracker = WorkTracker() - - # Create overlay window - this will be the visible always-on-top window - overlay = OverlayWindow(root, tracker, tracker.config) - - # Start main tracking loop in background - main_loop_thread = threading.Thread(target=tracker.run_main_loop, daemon=True) - main_loop_thread.start() - - # Set up hotkeys +def setup_hotkeys(tracker, overlay): try: from pynput import keyboard - def on_hotkey(key): - try: - if key == keyboard.Key.alt_l: # Left Alt pressed - return # Wait for combination - elif hasattr(key, 'char') and key.char == 't': - # Ctrl+Alt+T: Toggle tracking - tracker.toggle_tracking() - overlay.update_display() - elif hasattr(key, 'char') and key.char == 'n': - # Ctrl+Alt+N: New task - overlay.new_task() - elif hasattr(key, 'char') and key.char == 's': - # Ctrl+Alt+S: End session - if tracker.session_manager.current_session: - tracker.session_manager.end_current_session() - overlay.update_display() - elif hasattr(key, 'char') and key.char == 'm': - # Ctrl+Alt+M: Adjust time - overlay.adjust_time() - except AttributeError: - # Special keys like Ctrl, Alt don't have char attribute - pass + def on_toggle(): + tracker.toggle_tracking() + overlay.update_display() - # Start keyboard listener - keyboard.Listener(on_press=on_hotkey).start() + def on_new_task(): + overlay.new_task() + + def on_end_session(): + if tracker.session_manager.current_session: + tracker.session_manager.end_current_session() + overlay.update_display() + + def on_adjust(): + overlay.adjust_time() + + hotkey_map = { + '++t': on_toggle, + '++n': on_new_task, + '++s': on_end_session, + '++m': on_adjust, + } + + listener = keyboard.GlobalHotKeys(hotkey_map) + listener.start() + return listener except ImportError: print("pynput not available, hotkeys disabled") + return None + + +def main(): + root = tk.Tk() + root.title("Work Tracker") + root.geometry("1x1+2000+2000") + + tracker = WorkTracker() + overlay = OverlayWindow(root, tracker, tracker.config) + + # Inject notification callbacks + tracker.show_distraction_notification = lambda title: overlay.show_notification("Отвлечение", f"Вы используете: {title}") + tracker.show_neutral_notification = lambda title: overlay.show_notification("Нейтральное приложение", f"Окно: {title}") + + main_loop_thread = threading.Thread(target=tracker.run_main_loop, daemon=True) + main_loop_thread.start() + + hotkey_listener = setup_hotkeys(tracker, overlay) - # Run the GUI try: root.mainloop() except KeyboardInterrupt: print("\nShutting down...") - # End any current session before exiting if tracker.session_manager.current_session: tracker.session_manager.end_current_session() diff --git a/src/time_tracker/platform/windows.py b/src/time_tracker/platform/windows.py index c571e0d..6c6fa1a 100644 --- a/src/time_tracker/platform/windows.py +++ b/src/time_tracker/platform/windows.py @@ -1,42 +1,45 @@ import win32gui +import win32process import win32con import win32api import threading from typing import Callable +import psutil def get_active_window() -> str: - """Get the currently active window title on Windows""" + """Returns a string like 'YouTube - Chrome (chrome.exe)'""" try: hwnd = win32gui.GetForegroundWindow() - return win32gui.GetWindowText(hwnd) or "Unknown Window" - except: - return "Error getting window title" + title = win32gui.GetWindowText(hwnd) or "Unknown Window" + + # Get process name + _, pid = win32process.GetWindowThreadProcessId(hwnd) + try: + proc = psutil.Process(pid) + exe_name = proc.name() + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + exe_name = "unknown.exe" + + return f"{title} ({exe_name})" + except Exception: + return "Error getting window info" 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): - # 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 + # Fallback numeric constants + 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") + 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) @@ -52,33 +55,22 @@ def register_session_listener(callback: Callable[[str], None]) -> None: except: pass # Already registered - # 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, + "Session Watcher", + 0, 0, 0, 0, 0, + 0, 0, hinst, None ) # Register for session notifications try: from win32ts import WTSRegisterSessionNotification - WTSRegisterSessionNotification(hwnd, 1) # NOTIFY_FOR_THIS_SESSION + WTSRegisterSessionNotification(hwnd, 1) except ImportError: - # pywin32 might not have win32ts on all systems pass - # Start message loop win32gui.PumpMessages() - # Run in a separate thread thread = threading.Thread(target=run_message_loop, daemon=True) thread.start() @@ -95,4 +87,4 @@ def get_idle_time() -> int: windll.user32.GetLastInputInfo(byref(lastInputInfo)) millis = windll.kernel32.GetTickCount() - lastInputInfo.dwTime - return millis // 1000 # Convert milliseconds to seconds \ No newline at end of file + return millis // 1000 \ No newline at end of file diff --git a/src/time_tracker/ui/overlay.py b/src/time_tracker/ui/overlay.py index 5d7942f..7603014 100644 --- a/src/time_tracker/ui/overlay.py +++ b/src/time_tracker/ui/overlay.py @@ -2,7 +2,7 @@ import tkinter as tk from tkinter import simpledialog, messagebox import threading import time -from typing import Any, Callable +from typing import Any class OverlayWindow: @@ -11,108 +11,87 @@ class OverlayWindow: self.tracker = tracker self.config = config - # Configure window to be always on top + # Always on top, no decorations self.root.attributes("-topmost", True) - self.root.overrideredirect(True) # Remove window decorations + self.root.overrideredirect(True) + self.root.geometry("320x180+100+100") - # Force the geometry to be set correctly - self.root.geometry("300x150+100+100") + # Semi-transparent by default + self.root.wm_attributes("-alpha", 0.7) + + # Hover effects + self.root.bind("", self.on_hover) + self.root.bind("", self.on_leave) - # Create UI elements self.create_widgets() - - # Bind drag functionality - self.setup_drag() - - # Bind right-click to show context menu for closing + self.setup_drag_full() self.root.bind("", self.show_context_menu) - # Update display periodically + # Start periodic updates self.update_display_periodically() + def on_hover(self, event=None): + self.root.wm_attributes("-alpha", 1.0) + + def on_leave(self, event=None): + self.root.wm_attributes("-alpha", 0.7) + + def setup_drag_full(self): + """Allow dragging the entire window from any point.""" + def start_move(event): + self._drag_x = event.x + self._drag_y = event.y + + def do_move(event): + x = self.root.winfo_x() + (event.x - self._drag_x) + y = self.root.winfo_y() + (event.y - self._drag_y) + self.root.geometry(f"+{x}+{y}") + + self.root.bind("", start_move) + self.root.bind("", do_move) + def create_widgets(self): - """Create all UI widgets""" - # Main frame with visible background main_frame = tk.Frame(self.root, bg='#f0f0f0', bd=2, relief='solid') main_frame.pack(fill='both', expand=True, padx=2, pady=2) - # Status line (tracking enabled/disabled) - self.status_label = tk.Label(main_frame, text="", bg='#f0f0f0', font=('Arial', 10)) + self.status_label = tk.Label(main_frame, text="", bg='#f0f0f0', font=('Arial', 9)) self.status_label.pack(anchor='w', padx=5, pady=2) - # Task info self.task_label = tk.Label(main_frame, text="", bg='#f0f0f0', font=('Arial', 10, 'bold')) self.task_label.pack(anchor='w', padx=5, pady=2) - # Window info self.window_label = tk.Label(main_frame, text="", bg='#f0f0f0', font=('Arial', 9)) self.window_label.pack(anchor='w', padx=5, pady=2) - # Total time info self.total_label = tk.Label(main_frame, text="", bg='#f0f0f0', font=('Arial', 9)) self.total_label.pack(anchor='w', padx=5, pady=2) - # Buttons frame buttons_frame = tk.Frame(main_frame, bg='#f0f0f0') buttons_frame.pack(fill='x', padx=5, pady=5) - # Tracking toggle button - self.track_btn = tk.Button(buttons_frame, text="●", command=self.toggle_tracking, width=3) + self.track_btn = tk.Button(buttons_frame, text="▶", command=self.toggle_tracking, width=8) self.track_btn.pack(side='left', padx=2) - # New task button - new_task_btn = tk.Button(buttons_frame, text="N", command=self.new_task, width=3) - new_task_btn.pack(side='left', padx=2) - - # Switch task button - switch_task_btn = tk.Button(buttons_frame, text="S", command=self.switch_task, width=3) - switch_task_btn.pack(side='left', padx=2) - - # Adjust time button - adjust_btn = tk.Button(buttons_frame, text="±", command=self.adjust_time, width=3) - adjust_btn.pack(side='left', padx=2) - - def setup_drag(self): - """Setup window dragging functionality""" - - def start_move(event): - self.root.x = event.x - self.root.y = event.y - - def do_move(event): - x = self.root.winfo_x() + (event.x - self.root.x) - y = self.root.winfo_y() + (event.y - self.root.y) - self.root.geometry(f"+{x}+{y}") - - # Bind drag events to the main label (covers entire window) - self.status_label.bind("", start_move) - self.status_label.bind("", do_move) - - # Also bind to other labels for full coverage - for widget in [self.task_label, self.window_label, self.total_label]: - widget.bind("", start_move) - widget.bind("", do_move) + tk.Button(buttons_frame, text="+Задача", command=self.new_task, width=8).pack(side='left', padx=2) + tk.Button(buttons_frame, text="↔Задача", command=self.switch_task, width=8).pack(side='left', padx=2) + tk.Button(buttons_frame, text="±Время", command=self.adjust_time, width=8).pack(side='left', padx=2) def show_context_menu(self, event): - """Show context menu with close option""" context_menu = tk.Menu(self.root, tearoff=0) context_menu.add_command(label="Закрыть", command=self.root.quit) context_menu.post(event.x_root, event.y_root) def toggle_tracking(self): - """Toggle tracking state""" self.tracker.toggle_tracking() self.update_display() def new_task(self): - """Create a new task""" task_id = simpledialog.askstring("Новая задача", "Введите ID задачи (например, PRJ-123):") - if task_id: - self.tracker.create_task(task_id) + if task_id and task_id.strip(): + self.tracker.create_task(task_id.strip()) self.update_display() def switch_task(self): - """Switch to an existing task""" available_tasks = self.tracker.get_available_tasks() if not available_tasks: messagebox.showinfo("Переключить задачу", "Нет доступных задач") @@ -124,146 +103,117 @@ class OverlayWindow: f"Доступные задачи:\n{task_list}\n\nВведите ID задачи:" ) - if selected_task and selected_task in available_tasks: - self.tracker.switch_task(selected_task) + if selected_task and selected_task.strip() in available_tasks: + self.tracker.switch_task(selected_task.strip()) self.update_display() def adjust_time(self): - """Show dialog to adjust time manually""" self.show_adjust_time_dialog() def show_adjust_time_dialog(self): - """Show dialog for manual time adjustment""" adjustment_window = tk.Toplevel(self.root) adjustment_window.title("Ручная корректировка времени") - adjustment_window.geometry("300x150") + adjustment_window.geometry("350x200") adjustment_window.transient(self.root) - adjustment_window.grab_set() # Modal window + adjustment_window.grab_set() - # Operation selection operation_var = tk.StringVar(value="+") - tk.Radiobutton(adjustment_window, text="Добавить время", variable=operation_var, value="+").pack(anchor='w', - padx=10, - pady=5) - tk.Radiobutton(adjustment_window, text="Вычесть время", variable=operation_var, value="-").pack(anchor='w', - padx=10, pady=5) + tk.Radiobutton(adjustment_window, text="Добавить время", variable=operation_var, value="+").pack(anchor='w', padx=10, pady=2) + tk.Radiobutton(adjustment_window, text="Вычесть время", variable=operation_var, value="-").pack(anchor='w', padx=10, pady=2) - # Minutes entry - tk.Label(adjustment_window, text="Минут:").pack(anchor='w', padx=10) + tk.Label(adjustment_window, text="Минуты:").pack(anchor='w', padx=10, pady=(10,0)) minutes_entry = tk.Entry(adjustment_window) - minutes_entry.pack(padx=10, pady=5) - minutes_entry.insert(0, "15") # Default value + minutes_entry.pack(padx=10, pady=2) + minutes_entry.insert(0, "15") - # Reason entry - tk.Label(adjustment_window, text="Причина:").pack(anchor='w', padx=10) + tk.Label(adjustment_window, text="Причина:").pack(anchor='w', padx=10, pady=(10,0)) reason_entry = tk.Entry(adjustment_window) - reason_entry.pack(padx=10, pady=5) - reason_entry.insert(0, "Ручная корректировка") # Default value + reason_entry.pack(padx=10, pady=2) + reason_entry.insert(0, "Перерыв / встреча") def apply_adjustment(): try: minutes = int(minutes_entry.get()) + if minutes <= 0: + raise ValueError("Minutes must be positive") operation = operation_var.get() - seconds = minutes * 60 - if operation == "-": - seconds = -seconds - - reason = reason_entry.get() + seconds = minutes * 60 * (1 if operation == "+" else -1) + reason = reason_entry.get().strip() + if not reason: + reason = "Без причины" if self.tracker.has_active_session(): self.tracker.add_manual_adjustment(seconds, reason) - messagebox.showinfo("Успешно", f"Время скорректировано: {operation}{minutes} мин") + messagebox.showinfo("Успех", f"Изменено: {operation}{minutes} мин\nПричина: {reason}") adjustment_window.destroy() self.update_display() else: - messagebox.showwarning("Внимание", "Нет активной сессии для корректировки") + messagebox.showwarning("Ошибка", "Нет активной сессии!") except ValueError: - messagebox.showerror("Ошибка", "Введите корректное число минут") + messagebox.showerror("Ошибка", "Введите положительное число минут") - # Apply button - apply_btn = tk.Button(adjustment_window, text="Применить", command=apply_adjustment) - apply_btn.pack(pady=10) + tk.Button(adjustment_window, text="Применить", command=apply_adjustment, bg='lightblue').pack(pady=10) def update_display(self): - """Update the display with current information""" - # Update tracking status - track_status = "●" if self.config.tracking_enabled else "○" - status_text = f"[{track_status}] Трекинг: {'ВКЛ' if self.config.tracking_enabled else 'ВЫКЛ'}" - self.status_label.config(text=status_text) + # Use lock to avoid race conditions + with self.tracker._state_lock: + track_status = "▶" if self.config.tracking_enabled else "⏸" + status_text = f"[{track_status}] Трекинг: {'ВКЛ' if self.config.tracking_enabled else 'ВЫКЛ'}" + self.status_label.config(text=status_text) - # Update task information - if self.config.current_task_id: - # Get current session duration - current_duration = self.tracker.get_current_session_duration() - current_duration_str = self.format_duration(current_duration) - - # Get planned duration if available - planned_duration = self.tracker.get_planned_duration(self.config.current_task_id) - if planned_duration: - planned_str = self.format_duration(planned_duration * 60) # Convert minutes to seconds - task_text = f"Задача: {self.config.current_task_id} | {current_duration_str} / {planned_str}" + if self.config.current_task_id: + current_duration = self.tracker.get_current_session_duration() + current_str = self.format_duration(current_duration) + task_text = f"Задача: {self.config.current_task_id} | {current_str}" else: - task_text = f"Задача: {self.config.current_task_id} | {current_duration_str}" - else: - task_text = "Нет активной задачи" + task_text = "Нет активной задачи" + self.task_label.config(text=task_text) - self.task_label.config(text=task_text) - - # Update window information - if hasattr(self.tracker, 'active_window'): window_title = getattr(self.tracker, 'active_window', 'Неизвестно') classification = getattr(self.tracker, 'window_classification', 'neutral') - class_symbol = {"work": "✅", "distraction": "⚠", "neutral": "⚪"}[classification] - window_text = f"Окно: {window_title[:30]}{'...' if len(window_title) > 30 else ''} {class_symbol}" + display_title = window_title[:40] + '...' if len(window_title) > 40 else window_title + window_text = f"Окно: {display_title} {class_symbol}" self.window_label.config(text=window_text) - else: - self.window_label.config(text="Окно: Неизвестно") - # Update total time today - total_today = self.tracker.get_total_work_today() - total_str = self.format_duration(total_today) - self.total_label.config(text=f"Всего сегодня: {total_str}") + total_today = self.tracker.get_total_work_today() + total_str = self.format_duration(total_today) + self.total_label.config(text=f"Сегодня: {total_str}") - # Update button colors based on tracking status - if self.config.tracking_enabled: - self.track_btn.config(bg='lightgreen', text="●") - else: - self.track_btn.config(bg='lightcoral', text="○") + # Update button appearance + self.track_btn.config( + text="⏸" if self.config.tracking_enabled else "▶", + bg='lightgreen' if self.config.tracking_enabled else 'lightcoral' + ) def format_duration(self, seconds: int) -> str: - """Format seconds into human-readable string (h:mm)""" + """Format as H:MM:SS or M:SS""" if seconds < 0: seconds = 0 - - hours = seconds // 3600 - minutes = (seconds % 3600) // 60 - + hours, remainder = divmod(seconds, 3600) + minutes, secs = divmod(remainder, 60) if hours > 0: - return f"{hours}ч{minutes:02d}м" + return f"{hours}:{minutes:02d}:{secs:02d}" else: - return f"{minutes}м" + return f"{minutes}:{secs:02d}" def update_display_periodically(self): - """Update display every second""" self.update_display() self.root.after(1000, self.update_display_periodically) def show_notification(self, title: str, message: str, duration: int = 5000): - """Show a temporary notification""" - # Create notification window + """Show a temporary notification window""" notif = tk.Toplevel(self.root) notif.title(title) - notif.geometry("250x80+200+200") + notif.geometry("280x90+200+200") notif.overrideredirect(True) notif.attributes("-topmost", True) + notif.wm_attributes("-alpha", 0.9) - # Style similar to main window frame = tk.Frame(notif, bg='lightyellow', bd=2, relief='solid') frame.pack(fill='both', expand=True) tk.Label(frame, text=title, bg='lightyellow', font=('Arial', 10, 'bold')).pack(pady=5) - tk.Label(frame, text=message, bg='lightyellow', font=('Arial', 9)).pack(pady=5) + tk.Label(frame, text=message[:80], bg='lightyellow', font=('Arial', 9), wraplength=260).pack(pady=5) - # Close after specified duration notif.after(duration, notif.destroy) \ No newline at end of file