Created
January 4, 2026 11:33
-
-
Save Pikachuxxxx/3832bae7a4183045dce3ff1f04370ac6 to your computer and use it in GitHub Desktop.
daily productivity tracker app for myself, I hate writing status updates so this (VibeCoded with 5.1 Codex Max)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| from __future__ import annotations | |
| import configparser | |
| import random | |
| import subprocess | |
| import sys | |
| import threading | |
| from dataclasses import dataclass | |
| from datetime import date, datetime, timedelta | |
| from pathlib import Path | |
| from typing import Dict, List, Optional | |
| from PyQt6 import QtCore, QtGui, QtWidgets | |
| DATA_DIR = Path.home() / ".prod_tracker" | |
| CONFIG_PATH = DATA_DIR / "config.ini" | |
| HOURLY_INTERVAL_MINUTES = 60 | |
| def ensure_data_dir() -> None: | |
| DATA_DIR.mkdir(parents=True, exist_ok=True) | |
| def format_duration(seconds: float) -> str: | |
| secs = int(seconds) | |
| hrs, rem = divmod(secs, 3600) | |
| mins, rem = divmod(rem, 60) | |
| return f"{hrs:02d}:{mins:02d}:{rem:02d}" | |
| @dataclass | |
| class CommandEntry: | |
| name: str | |
| command: str | |
| class StorageManager: | |
| def __init__(self) -> None: | |
| ensure_data_dir() | |
| self.config = configparser.ConfigParser() | |
| if CONFIG_PATH.exists(): | |
| self.config.read(CONFIG_PATH) | |
| if "commands" not in self.config: | |
| self.config["commands"] = {} | |
| self.day_file_path = self._day_file_path(date.today()) | |
| def _day_file_path(self, day: date) -> Path: | |
| return DATA_DIR / f"hourly_logs_{day.strftime('%Y%m%d')}.txt" | |
| def write_daily_status(self, text: str) -> None: | |
| stamp = datetime.now().strftime("%Y-%m-%d %H:%M") | |
| header = ( | |
| "========================================\n" | |
| f"DAILY STATUS @ {stamp}\n" | |
| "========================================\n" | |
| f"{text.strip()}\n" | |
| "========================================\n\n" | |
| ) | |
| self.day_file_path.write_text(header, encoding="utf-8") | |
| def append_hourly_log(self, entry: str) -> None: | |
| stamp = datetime.now().strftime("%Y-%m-%d %H:%M") | |
| with self.day_file_path.open("a", encoding="utf-8") as fh: | |
| fh.write( | |
| "++++++++ HOUR LOG ++++++++\n" | |
| f"[{stamp}]\n" | |
| f"{entry.strip()}\n" | |
| "+++++++++++++++++++++++++++\n\n" | |
| ) | |
| def append_break_entry(self, kind: str, duration_seconds: float) -> None: | |
| stamp = datetime.now().strftime("%Y-%m-%d %H:%M") | |
| with self.day_file_path.open("a", encoding="utf-8") as fh: | |
| fh.write(f"[BREAK] {stamp} kind={kind} duration={format_duration(duration_seconds)}\n") | |
| def append_day_summary(self, work_seconds: float, tasks: List[str]) -> None: | |
| stamp = datetime.now().strftime("%Y-%m-%d %H:%M") | |
| with self.day_file_path.open("a", encoding="utf-8") as fh: | |
| fh.write("[DAY_END] {stamp}\n".format(stamp=stamp)) | |
| fh.write(f"total_work={format_duration(work_seconds)}\n") | |
| if tasks: | |
| fh.write("remaining_tasks=\n") | |
| for t in tasks: | |
| fh.write(f"- {t}\n") | |
| fh.write("\n") | |
| def load_commands(self) -> List[CommandEntry]: | |
| cmds: List[CommandEntry] = [] | |
| if self.config.has_section("commands"): | |
| for name, cmd in self.config.items("commands"): | |
| cmds.append(CommandEntry(name=name, command=cmd)) | |
| return cmds | |
| def save_commands(self, entries: List[CommandEntry]) -> None: | |
| self.config["commands"] = {c.name: c.command for c in entries} | |
| with CONFIG_PATH.open("w", encoding="utf-8") as fh: | |
| self.config.write(fh) | |
| def weekly_report_path(self, day: date) -> Path: | |
| iso_year, iso_week, _ = day.isocalendar() | |
| return DATA_DIR / f"weekly_report_{iso_year}_week{iso_week}.txt" | |
| def generate_weekly_report(self, day: date) -> None: | |
| iso_year, iso_week, _ = day.isocalendar() | |
| lines: List[str] = [] | |
| for file in DATA_DIR.glob("hourly_logs_*.txt"): | |
| name = file.stem.replace("hourly_logs_", "") | |
| try: | |
| file_day = datetime.strptime(name, "%Y%m%d").date() | |
| except ValueError: | |
| continue | |
| y, w, _ = file_day.isocalendar() | |
| if y == iso_year and w == iso_week: | |
| lines.append(f"# {file_day}\n") | |
| lines.extend(file.read_text(encoding="utf-8").splitlines()) | |
| lines.append("\n") | |
| report_path = self.weekly_report_path(day) | |
| if lines: | |
| report_path.write_text("\n".join(lines), encoding="utf-8") | |
| class FullScreenPrompt(QtWidgets.QDialog): | |
| submitted = QtCore.pyqtSignal(str) | |
| def __init__(self, title: str, placeholder: str, parent: Optional[QtWidgets.QWidget] = None) -> None: | |
| super().__init__(parent) | |
| self.setWindowTitle(title) | |
| self.setModal(True) | |
| self.setWindowFlag(QtCore.Qt.WindowType.WindowStaysOnTopHint, True) | |
| self.setWindowFlag(QtCore.Qt.WindowType.FramelessWindowHint, True) | |
| self.showFullScreen() | |
| layout = QtWidgets.QVBoxLayout() | |
| self.label = QtWidgets.QLabel(title) | |
| self.label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) | |
| self.label.setStyleSheet("color: #0f0; font-size: 28px; font-weight: bold;") | |
| layout.addWidget(self.label) | |
| self.text = QtWidgets.QPlainTextEdit() | |
| self.text.setPlaceholderText(placeholder) | |
| self.text.setStyleSheet("font-size: 18px; background: #111; color: #fff; padding: 16px;") | |
| layout.addWidget(self.text, 1) | |
| self.submit_btn = QtWidgets.QPushButton("Submit") | |
| self.submit_btn.setStyleSheet("background: #0a0; color: #fff; font-size: 22px; padding: 16px;") | |
| self.submit_btn.clicked.connect(self._on_submit) | |
| layout.addWidget(self.submit_btn) | |
| self.setLayout(layout) | |
| def _on_submit(self) -> None: | |
| content = self.text.toPlainText().strip() | |
| if content: | |
| self.submitted.emit(content) | |
| self.accept() | |
| def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa: N802 | |
| if event.key() in (QtCore.Qt.Key.Key_Escape, QtCore.Qt.Key.Key_Q): | |
| return | |
| super().keyPressEvent(event) | |
| class BreakOverlay(QtWidgets.QDialog): | |
| ended = QtCore.pyqtSignal(float) | |
| def __init__(self, label: str, target_minutes: Optional[int] = None, parent: Optional[QtWidgets.QWidget] = None) -> None: | |
| super().__init__(parent) | |
| self.setModal(True) | |
| self.setWindowFlag(QtCore.Qt.WindowType.WindowStaysOnTopHint, True) | |
| self.setWindowFlag(QtCore.Qt.WindowType.FramelessWindowHint, True) | |
| self.showFullScreen() | |
| self.started_at = datetime.now() | |
| self.target_minutes = target_minutes | |
| layout = QtWidgets.QVBoxLayout() | |
| self.label = QtWidgets.QLabel(f"Break: {label}") | |
| self.label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) | |
| self.label.setStyleSheet("color: #f33; font-size: 32px; font-weight: bold;") | |
| layout.addWidget(self.label) | |
| self.timer_label = QtWidgets.QLabel("00:00:00") | |
| self.timer_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) | |
| self.timer_label.setStyleSheet("color: #fff; font-size: 26px;") | |
| layout.addWidget(self.timer_label) | |
| self.end_btn = QtWidgets.QPushButton("End Break") | |
| self.end_btn.setStyleSheet("background: #a00; color: #fff; font-size: 24px; padding: 18px;") | |
| self.end_btn.clicked.connect(self.finish) | |
| layout.addWidget(self.end_btn) | |
| self.setLayout(layout) | |
| self.update_timer = QtCore.QTimer(self) | |
| self.update_timer.timeout.connect(self._tick) | |
| self.update_timer.start(1000) | |
| def _tick(self) -> None: | |
| elapsed = (datetime.now() - self.started_at).total_seconds() | |
| self.timer_label.setText(format_duration(elapsed)) | |
| if self.target_minutes: | |
| remaining = int(self.target_minutes * 60 - elapsed) | |
| if remaining <= 0: | |
| self.timer_label.setStyleSheet("color: #0f0; font-size: 26px;") | |
| def finish(self) -> None: | |
| elapsed = (datetime.now() - self.started_at).total_seconds() | |
| self.ended.emit(elapsed) | |
| self.accept() | |
| def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: # noqa: N802 | |
| if event.key() in (QtCore.Qt.Key.Key_Escape, QtCore.Qt.Key.Key_Q): | |
| return | |
| super().keyPressEvent(event) | |
| class MainWindow(QtWidgets.QMainWindow): | |
| def __init__(self) -> None: | |
| super().__init__() | |
| self.setWindowTitle("Prod Tracker") | |
| self.resize(1100, 720) | |
| self.setStyleSheet( | |
| """ | |
| QMainWindow { background: #0c0c0c; color: #eaeaea; } | |
| QLabel { color: #eaeaea; } | |
| QPushButton { background: #1b1b1b; color: #eaeaea; border: 1px solid #333; padding: 8px; } | |
| QPushButton:hover { background: #222; } | |
| QListWidget, QPlainTextEdit, QLineEdit, QTextEdit { background: #111; color: #eaeaea; border: 1px solid #222; } | |
| QTabWidget::pane { border: 1px solid #222; } | |
| """ | |
| ) | |
| self.storage = StorageManager() | |
| self.day_started = False | |
| self.day_ended = False | |
| self.start_time: Optional[datetime] = None | |
| self.total_break_seconds = 0.0 | |
| self.active_break_overlay: Optional[BreakOverlay] = None | |
| self.daily_status_text = "" | |
| self.work_timer = QtCore.QTimer(self) | |
| self.work_timer.timeout.connect(self._update_work_timer) | |
| self.hourly_timer = QtCore.QTimer(self) | |
| self.hourly_timer.setInterval(HOURLY_INTERVAL_MINUTES * 60 * 1000) | |
| self.hourly_timer.timeout.connect(self.prompt_hourly_log) | |
| self._build_ui() | |
| def _build_ui(self) -> None: | |
| central = QtWidgets.QWidget() | |
| root_layout = QtWidgets.QHBoxLayout() | |
| central.setLayout(root_layout) | |
| # Main tabs | |
| self.tabs = QtWidgets.QTabWidget() | |
| root_layout.addWidget(self.tabs, 2) | |
| # Today tab | |
| today_tab = QtWidgets.QWidget() | |
| today_layout = QtWidgets.QVBoxLayout() | |
| today_tab.setLayout(today_layout) | |
| self.start_btn = QtWidgets.QPushButton("START DAY") | |
| self.start_btn.setStyleSheet("background: #0c7a0c; color: #fff; font-size: 22px; padding: 16px;") | |
| self.start_btn.clicked.connect(self.start_day) | |
| today_layout.addWidget(self.start_btn) | |
| self.timer_label = QtWidgets.QLabel("00:00:00") | |
| self.timer_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) | |
| self.timer_label.setStyleSheet( | |
| "font-size: 48px; color: #0f0; padding: 12px;" | |
| "background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #0a0a0a, stop:1 #111);" | |
| "border: 1px solid #0a0; border-radius: 8px;" | |
| ) | |
| today_layout.addWidget(self.timer_label) | |
| break_row = QtWidgets.QHBoxLayout() | |
| self.lunch_btn = QtWidgets.QPushButton("Lunch (30m)") | |
| self.lunch_btn.clicked.connect(lambda: self.start_break("Lunch", 30)) | |
| break_row.addWidget(self.lunch_btn) | |
| self.water_btn = QtWidgets.QPushButton("Water (5m)") | |
| self.water_btn.clicked.connect(lambda: self.start_break("Water", 5)) | |
| break_row.addWidget(self.water_btn) | |
| self.random_btn = QtWidgets.QPushButton("Random (5-15m)") | |
| self.random_btn.clicked.connect(self._random_break) | |
| break_row.addWidget(self.random_btn) | |
| self.custom_break_btn = QtWidgets.QPushButton("Custom Break") | |
| self.custom_break_btn.clicked.connect(lambda: self.start_break("Custom", None)) | |
| break_row.addWidget(self.custom_break_btn) | |
| today_layout.addLayout(break_row) | |
| self.test_prompt_btn = QtWidgets.QPushButton("Test Hourly Pop-up") | |
| self.test_prompt_btn.setStyleSheet("background: #444; color: #fff; font-size: 16px; padding: 10px;") | |
| self.test_prompt_btn.setToolTip("Force a full-screen hourly log prompt to grab attention") | |
| self.test_prompt_btn.clicked.connect(lambda: self.prompt_hourly_log(force=True)) | |
| today_layout.addWidget(self.test_prompt_btn) | |
| self.log_view = QtWidgets.QTextEdit() | |
| self.log_view.setReadOnly(True) | |
| self.log_view.setPlaceholderText("Hourly logs will appear here") | |
| today_layout.addWidget(self.log_view, 1) | |
| self.end_btn = QtWidgets.QPushButton("END DAY") | |
| self.end_btn.setEnabled(False) | |
| self.end_btn.setStyleSheet("background: #a00; color: #fff; font-size: 20px; padding: 14px;") | |
| self.end_btn.clicked.connect(self.end_day) | |
| today_layout.addWidget(self.end_btn) | |
| self.tabs.addTab(today_tab, "Today") | |
| # Commands tab | |
| cmd_tab = QtWidgets.QWidget() | |
| cmd_layout = QtWidgets.QVBoxLayout() | |
| cmd_tab.setLayout(cmd_layout) | |
| form_row = QtWidgets.QHBoxLayout() | |
| self.cmd_name_input = QtWidgets.QLineEdit() | |
| self.cmd_name_input.setPlaceholderText("Command name") | |
| form_row.addWidget(self.cmd_name_input) | |
| self.cmd_input = QtWidgets.QLineEdit() | |
| self.cmd_input.setPlaceholderText("Command (e.g. open -a Notes)") | |
| form_row.addWidget(self.cmd_input, 2) | |
| self.add_cmd_btn = QtWidgets.QPushButton("Add/Update") | |
| self.add_cmd_btn.clicked.connect(self.add_command) | |
| form_row.addWidget(self.add_cmd_btn) | |
| cmd_layout.addLayout(form_row) | |
| self.cmd_list = QtWidgets.QListWidget() | |
| self.cmd_list.itemSelectionChanged.connect(self._load_selected_command) | |
| cmd_layout.addWidget(self.cmd_list, 1) | |
| cmd_btn_row = QtWidgets.QHBoxLayout() | |
| self.run_cmd_btn = QtWidgets.QPushButton("Run Selected") | |
| self.run_cmd_btn.clicked.connect(self.run_selected_command) | |
| cmd_btn_row.addWidget(self.run_cmd_btn) | |
| self.del_cmd_btn = QtWidgets.QPushButton("Delete Selected") | |
| self.del_cmd_btn.clicked.connect(self.delete_selected_command) | |
| cmd_btn_row.addWidget(self.del_cmd_btn) | |
| cmd_layout.addLayout(cmd_btn_row) | |
| self.cmd_output = QtWidgets.QPlainTextEdit() | |
| self.cmd_output.setReadOnly(True) | |
| self.cmd_output.setPlaceholderText("Command output") | |
| cmd_layout.addWidget(self.cmd_output, 1) | |
| self.tabs.addTab(cmd_tab, "Commands") | |
| # Right panel for daily stack | |
| side_panel = QtWidgets.QVBoxLayout() | |
| root_layout.addLayout(side_panel, 1) | |
| side_panel.addWidget(QtWidgets.QLabel("Daily Stack")) | |
| self.stack_input = QtWidgets.QLineEdit() | |
| self.stack_input.setPlaceholderText("Add a task and press Enter") | |
| self.stack_input.returnPressed.connect(self.add_stack_item) | |
| side_panel.addWidget(self.stack_input) | |
| self.stack_list = QtWidgets.QListWidget() | |
| self.stack_list.setSpacing(6) | |
| side_panel.addWidget(self.stack_list, 1) | |
| stack_btn_row = QtWidgets.QHBoxLayout() | |
| self.stack_up_btn = QtWidgets.QPushButton("Up") | |
| self.stack_up_btn.clicked.connect(lambda: self.move_stack_item(-1)) | |
| stack_btn_row.addWidget(self.stack_up_btn) | |
| self.stack_down_btn = QtWidgets.QPushButton("Down") | |
| self.stack_down_btn.clicked.connect(lambda: self.move_stack_item(1)) | |
| stack_btn_row.addWidget(self.stack_down_btn) | |
| side_panel.addLayout(stack_btn_row) | |
| self.setCentralWidget(central) | |
| self._load_commands_into_ui() | |
| def start_day(self) -> None: | |
| if self.day_started: | |
| return | |
| prompt = FullScreenPrompt("Start Day - Daily Status", "Write today's intention / status") | |
| prompt.submitted.connect(self._receive_daily_status) | |
| prompt.exec() | |
| def _receive_daily_status(self, text: str) -> None: | |
| self.daily_status_text = text | |
| self.storage.write_daily_status(text) | |
| self.day_started = True | |
| self.start_time = datetime.now() | |
| self.work_timer.start(1000) | |
| self.hourly_timer.start() | |
| self.start_btn.setEnabled(False) | |
| self.end_btn.setEnabled(True) | |
| self.log_view.append("Day started. Timer running.") | |
| self._run_startup_commands() | |
| def _run_startup_commands(self) -> None: | |
| entries = self.storage.load_commands() | |
| if not entries: | |
| return | |
| def runner() -> None: | |
| self._append_log("[cmd] startup sequence ->") | |
| for entry in entries: | |
| self._append_command_output(f"$ {entry.command}\n") | |
| self._append_log(f"[cmd] {entry.name}: {entry.command}") | |
| try: | |
| proc = subprocess.Popen(entry.command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) | |
| out, _ = proc.communicate() | |
| if out: | |
| self._append_command_output(out.decode(errors="ignore")) | |
| except Exception as exc: # noqa: BLE001 | |
| self._append_command_output(f"Error running {entry.name}: {exc}\n") | |
| threading.Thread(target=runner, daemon=True).start() | |
| def _append_command_output(self, text: str) -> None: | |
| QtCore.QMetaObject.invokeMethod( | |
| self.cmd_output, | |
| "appendPlainText", | |
| QtCore.Qt.ConnectionType.QueuedConnection, | |
| QtCore.Q_ARG(str, text), | |
| ) | |
| def _append_log(self, text: str) -> None: | |
| QtCore.QMetaObject.invokeMethod( | |
| self.log_view, | |
| "append", | |
| QtCore.Qt.ConnectionType.QueuedConnection, | |
| QtCore.Q_ARG(str, text), | |
| ) | |
| def prompt_hourly_log(self, force: bool = False) -> None: | |
| if (not self.day_started or self.day_ended) and not force: | |
| return | |
| prompt = FullScreenPrompt("Hourly Log", "What did you finish this hour?") | |
| prompt.submitted.connect(self._receive_hourly_log) | |
| prompt.exec() | |
| def _receive_hourly_log(self, text: str) -> None: | |
| self.storage.append_hourly_log(text) | |
| stamp = datetime.now().strftime("%H:%M") | |
| flair = random.choice(["🚀", "🔥", "🧭", "📈", "✨", "🎯"]) | |
| bar = "+" * 26 | |
| pretty = f"{bar}\n{flair} [{stamp}]\n{text}\n{bar}" | |
| self.log_view.append(pretty) | |
| def _update_work_timer(self) -> None: | |
| if not self.start_time: | |
| return | |
| elapsed = (datetime.now() - self.start_time).total_seconds() | |
| active = max(0, elapsed - self.total_break_seconds) | |
| self.timer_label.setText(format_duration(active)) | |
| def _random_break(self) -> None: | |
| minutes = random.randint(5, 15) | |
| self.start_break("Random", minutes) | |
| def start_break(self, label: str, minutes: Optional[int]) -> None: | |
| if not self.day_started or self.day_ended: | |
| return | |
| if self.active_break_overlay: | |
| return | |
| overlay = BreakOverlay(label, minutes) | |
| overlay.ended.connect(lambda secs, l=label: self.finish_break(l, secs)) | |
| self.active_break_overlay = overlay | |
| overlay.exec() | |
| def finish_break(self, label: str, seconds: float) -> None: | |
| self.total_break_seconds += seconds | |
| self.active_break_overlay = None | |
| self.storage.append_break_entry(label, seconds) | |
| self.log_view.append(f"Break ended: {label} ({format_duration(seconds)})") | |
| def add_stack_item(self) -> None: | |
| text = self.stack_input.text().strip() | |
| if text: | |
| self._insert_stack_item(text) | |
| self.stack_input.clear() | |
| def move_stack_item(self, delta: int) -> None: | |
| row = self.stack_list.currentRow() | |
| if row < 0: | |
| return | |
| new_row = row + delta | |
| if 0 <= new_row < self.stack_list.count(): | |
| item = self.stack_list.takeItem(row) | |
| widget = self.stack_list.itemWidget(item) | |
| self.stack_list.removeItemWidget(item) | |
| self.stack_list.insertItem(new_row, item) | |
| self.stack_list.setItemWidget(item, widget) | |
| self.stack_list.setCurrentRow(new_row) | |
| def remove_stack_item(self) -> None: | |
| row = self.stack_list.currentRow() | |
| if row >= 0: | |
| item = self.stack_list.takeItem(row) | |
| widget = self.stack_list.itemWidget(item) | |
| self.stack_list.removeItemWidget(item) | |
| if widget: | |
| widget.deleteLater() | |
| del item | |
| def _load_commands_into_ui(self) -> None: | |
| self.cmd_list.clear() | |
| for entry in self.storage.load_commands(): | |
| self.cmd_list.addItem(entry.name) | |
| def _insert_stack_item(self, text: str, checked: bool = False) -> None: | |
| item = QtWidgets.QListWidgetItem() | |
| item.setSizeHint(QtCore.QSize(220, 42)) | |
| container = QtWidgets.QWidget() | |
| layout = QtWidgets.QHBoxLayout() | |
| layout.setContentsMargins(8, 4, 8, 4) | |
| layout.setSpacing(8) | |
| check_btn = QtWidgets.QPushButton("✓") | |
| check_btn.setCheckable(True) | |
| check_btn.setChecked(checked) | |
| check_btn.setFixedWidth(32) | |
| check_btn.setStyleSheet("background: #163; color: #fff; border-radius: 6px;") | |
| label = QtWidgets.QLabel(text) | |
| label.setStyleSheet("font-size: 14px; color: #eaeaea;") | |
| delete_btn = QtWidgets.QPushButton("✕") | |
| delete_btn.setFixedWidth(32) | |
| delete_btn.setStyleSheet("background: #611; color: #fff; border-radius: 6px;") | |
| layout.addWidget(check_btn) | |
| layout.addWidget(label, 1) | |
| layout.addWidget(delete_btn) | |
| container.setLayout(layout) | |
| def toggle_style(state: bool) -> None: | |
| label.setStyleSheet( | |
| "font-size: 14px; color: #8f8; text-decoration: line-through;" if state else "font-size: 14px; color: #eaeaea;" | |
| ) | |
| check_btn.toggled.connect(toggle_style) | |
| delete_btn.clicked.connect(lambda: self._delete_stack_item(item)) | |
| self.stack_list.addItem(item) | |
| self.stack_list.setItemWidget(item, container) | |
| toggle_style(checked) | |
| def _delete_stack_item(self, item: QtWidgets.QListWidgetItem) -> None: | |
| row = self.stack_list.row(item) | |
| if row >= 0: | |
| removed = self.stack_list.takeItem(row) | |
| widget = self.stack_list.itemWidget(removed) | |
| self.stack_list.removeItemWidget(removed) | |
| if widget: | |
| widget.deleteLater() | |
| del removed | |
| def _load_selected_command(self) -> None: | |
| item = self.cmd_list.currentItem() | |
| if not item: | |
| return | |
| name = item.text() | |
| entries = {c.name: c.command for c in self.storage.load_commands()} | |
| if name in entries: | |
| self.cmd_name_input.setText(name) | |
| self.cmd_input.setText(entries[name]) | |
| def add_command(self) -> None: | |
| name = self.cmd_name_input.text().strip() | |
| cmd = self.cmd_input.text().strip() | |
| if not name or not cmd: | |
| return | |
| entries = self.storage.load_commands() | |
| existing = [c for c in entries if c.name == name] | |
| if existing: | |
| existing[0].command = cmd | |
| else: | |
| entries.append(CommandEntry(name=name, command=cmd)) | |
| self.storage.save_commands(entries) | |
| self._load_commands_into_ui() | |
| self.cmd_name_input.clear() | |
| self.cmd_input.clear() | |
| def delete_selected_command(self) -> None: | |
| item = self.cmd_list.currentItem() | |
| if not item: | |
| return | |
| name = item.text() | |
| entries = [c for c in self.storage.load_commands() if c.name != name] | |
| self.storage.save_commands(entries) | |
| self._load_commands_into_ui() | |
| def run_selected_command(self) -> None: | |
| item = self.cmd_list.currentItem() | |
| if not item: | |
| return | |
| name = item.text() | |
| entries = {c.name: c.command for c in self.storage.load_commands()} | |
| cmd = entries.get(name) | |
| if not cmd: | |
| return | |
| def runner() -> None: | |
| self._append_command_output(f"$ {cmd}\n") | |
| try: | |
| proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) | |
| out, _ = proc.communicate() | |
| if out: | |
| self._append_command_output(out.decode(errors="ignore")) | |
| except Exception as exc: # noqa: BLE001 | |
| self._append_command_output(f"Error: {exc}\n") | |
| threading.Thread(target=runner, daemon=True).start() | |
| def end_day(self) -> None: | |
| if not self.day_started or self.day_ended: | |
| return | |
| if self.stack_list.count() > 0: | |
| QtWidgets.QMessageBox.warning(self, "Daily Stack", "Clear the daily stack before ending the day.") | |
| return | |
| confirm = QtWidgets.QMessageBox.question(self, "End Day", "Export logs and stop for today?") | |
| if confirm != QtWidgets.QMessageBox.StandardButton.Yes: | |
| return | |
| self.day_ended = True | |
| self.work_timer.stop() | |
| self.hourly_timer.stop() | |
| active_seconds = 0.0 | |
| if self.start_time: | |
| active_seconds = (datetime.now() - self.start_time).total_seconds() - self.total_break_seconds | |
| self.storage.append_day_summary(active_seconds, []) | |
| # Export weekly on Mondays for the previous week | |
| if date.today().isoweekday() == 1: | |
| self.storage.generate_weekly_report(date.today() - timedelta(days=1)) | |
| self.log_view.append("Day ended. Wrapping up and closing... ✨") | |
| QtCore.QTimer.singleShot(800, self.close) | |
| self.end_btn.setEnabled(False) | |
| def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 | |
| if self.day_started and not self.day_ended: | |
| QtWidgets.QMessageBox.information(self, "Stay Focused", "You need to end the day from the app.") | |
| event.ignore() | |
| return | |
| super().closeEvent(event) | |
| def main() -> None: | |
| app = QtWidgets.QApplication(sys.argv) | |
| app.setWindowIcon(QtGui.QIcon.fromTheme("clock")) | |
| win = MainWindow() | |
| win.show() | |
| sys.exit(app.exec()) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment