From e3efe68604f88f9d2cbd41ddd0416cbd11d88ac4 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sun, 30 Nov 2025 21:20:17 +0100 Subject: [PATCH] refactor: rename folders to fix N999, INP001, S311 linting rules - Rename PYTHON/ to python_pkg/ (fix N999 uppercase folder) - Rename camelCase folders to snake_case: - randomJPG -> random_jpg - tagDivider -> tag_divider - downloadCats -> download_cats - keyboardCoop -> keyboard_coop - extractLinks -> extract_links - scapeWebsite -> scrape_website - Rename camelCase files: - generateJpeg.py -> generate_jpeg.py - tagDivider.py -> tag_divider.py - Rename poker-modifier-app to poker_modifier_app (fix INP001) - Add __init__.py to poker_modifier_app - Replace random module with secrets.SystemRandom (fix S311) - Fix S110 try-except-pass with contextlib.suppress - Update all imports and config references --- screen_locker/install_autostart.sh | 52 ++ screen_locker/install_systemd.sh | 30 ++ screen_locker/remove_autostart.sh | 33 ++ screen_locker/remove_systemd.sh | 19 + screen_locker/screen_lock.py | 686 +++++++++++++++++++++++++++ screen_locker/workout-locker.service | 14 + screen_locker/workout_log.json | 20 + 7 files changed, 854 insertions(+) create mode 100755 screen_locker/install_autostart.sh create mode 100755 screen_locker/install_systemd.sh create mode 100755 screen_locker/remove_autostart.sh create mode 100755 screen_locker/remove_systemd.sh create mode 100755 screen_locker/screen_lock.py create mode 100644 screen_locker/workout-locker.service create mode 100644 screen_locker/workout_log.json diff --git a/screen_locker/install_autostart.sh b/screen_locker/install_autostart.sh new file mode 100755 index 0000000..0a5b5ca --- /dev/null +++ b/screen_locker/install_autostart.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# Script to add screen locker to i3 autostart +# This will run the workout screen locker on system startup + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +SCREEN_LOCK_PATH="$SCRIPT_DIR/screen_lock.py" +I3_CONFIG="$HOME/.config/i3/config" + +# Check if screen_lock.py exists +if [ ! -f "$SCREEN_LOCK_PATH" ]; then + echo "Error: screen_lock.py not found at $SCREEN_LOCK_PATH" + exit 1 +fi + +# Make sure screen_lock.py is executable +chmod +x "$SCREEN_LOCK_PATH" + +# Check if i3 config exists +if [ ! -f "$I3_CONFIG" ]; then + echo "Error: i3 config not found at $I3_CONFIG" + echo "Please create i3 config first or specify correct path" + exit 1 +fi + +# Check if autostart line already exists +if grep -q "exec.*screen_lock.py" "$I3_CONFIG"; then + echo "Screen locker autostart already configured in i3 config" + echo "Current line:" + grep "exec.*screen_lock.py" "$I3_CONFIG" + read -p "Do you want to replace it? (y/n) " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + # Remove old line + sed -i '/exec.*screen_lock\.py/d' "$I3_CONFIG" + else + echo "Keeping existing configuration" + exit 0 + fi +fi + +# Add autostart line to i3 config +echo "" >> "$I3_CONFIG" +echo "# Workout screen locker on startup (demo mode)" >> "$I3_CONFIG" +echo "exec --no-startup-id python3 $SCREEN_LOCK_PATH" >> "$I3_CONFIG" + +echo "✓ Screen locker added to i3 autostart (demo mode)" +echo "✓ Configuration added to: $I3_CONFIG" +echo "" +echo "The screen locker will run on next i3 restart/login" +echo "" +echo "To test now, run: i3-msg restart" +echo "To switch to production mode later, edit $I3_CONFIG and add --production flag" diff --git a/screen_locker/install_systemd.sh b/screen_locker/install_systemd.sh new file mode 100755 index 0000000..ac8d973 --- /dev/null +++ b/screen_locker/install_systemd.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Install workout locker as a systemd user service + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +SERVICE_FILE="$SCRIPT_DIR/workout-locker.service" +USER_SERVICE_DIR="$HOME/.config/systemd/user" +SERVICE_NAME="workout-locker.service" + +# Create user systemd directory if it doesn't exist +mkdir -p "$USER_SERVICE_DIR" + +# Copy service file to user systemd directory +cp "$SERVICE_FILE" "$USER_SERVICE_DIR/$SERVICE_NAME" + +# Update the ExecStart path in the service file to use absolute path +sed -i "s|ExecStart=/usr/bin/python3.*|ExecStart=/usr/bin/python3 $SCRIPT_DIR/screen_lock.py|" "$USER_SERVICE_DIR/$SERVICE_NAME" + +# Reload systemd daemon +systemctl --user daemon-reload + +# Enable the service to start on login +systemctl --user enable "$SERVICE_NAME" + +echo "✓ Workout locker service installed" +echo "✓ Service will start automatically on next login" +echo "" +echo "To start now: systemctl --user start workout-locker" +echo "To check status: systemctl --user status workout-locker" +echo "To stop: systemctl --user stop workout-locker" +echo "To disable autostart: systemctl --user disable workout-locker" diff --git a/screen_locker/remove_autostart.sh b/screen_locker/remove_autostart.sh new file mode 100755 index 0000000..2507ffc --- /dev/null +++ b/screen_locker/remove_autostart.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# Script to remove screen locker from i3 autostart + +I3_CONFIG="$HOME/.config/i3/config" + +# Check if i3 config exists +if [ ! -f "$I3_CONFIG" ]; then + echo "Error: i3 config not found at $I3_CONFIG" + exit 1 +fi + +# Check if autostart line exists +if ! grep -q "exec.*screen_lock.py" "$I3_CONFIG"; then + echo "Screen locker autostart not found in i3 config" + exit 0 +fi + +# Show what will be removed +echo "Found screen locker configuration:" +grep -B1 "exec.*screen_lock.py" "$I3_CONFIG" +echo "" + +read -p "Remove screen locker from autostart? (y/n) " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + # Remove the autostart lines + sed -i '/# Workout screen locker on startup/d' "$I3_CONFIG" + sed -i '/exec.*screen_lock\.py/d' "$I3_CONFIG" + echo "✓ Screen locker removed from i3 autostart" + echo "Changes will take effect on next i3 restart" +else + echo "Cancelled" +fi diff --git a/screen_locker/remove_systemd.sh b/screen_locker/remove_systemd.sh new file mode 100755 index 0000000..a024938 --- /dev/null +++ b/screen_locker/remove_systemd.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Remove workout locker systemd service + +SERVICE_NAME="workout-locker.service" +USER_SERVICE_DIR="$HOME/.config/systemd/user" + +# Stop the service if running +systemctl --user stop "$SERVICE_NAME" 2>/dev/null + +# Disable the service +systemctl --user disable "$SERVICE_NAME" 2>/dev/null + +# Remove service file +rm -f "$USER_SERVICE_DIR/$SERVICE_NAME" + +# Reload systemd daemon +systemctl --user daemon-reload + +echo "✓ Workout locker service removed" diff --git a/screen_locker/screen_lock.py b/screen_locker/screen_lock.py new file mode 100755 index 0000000..ea1715f --- /dev/null +++ b/screen_locker/screen_lock.py @@ -0,0 +1,686 @@ +#!/usr/bin/env python3 +"""Screen locker with workout verification for Arch Linux / i3wm. + +Requires user to log their workout to unlock the screen. +""" + +from datetime import datetime, timezone +import json +import logging +import os +import sys +import tkinter as tk + +logging.basicConfig(level=logging.INFO) + +# Validation limits for workout data +MAX_DISTANCE_KM = 100 +MAX_TIME_MINUTES = 600 +MAX_PACE_MIN_PER_KM = 20 +MIN_EXERCISE_NAME_LEN = 3 +MAX_SETS = 20 +MAX_REPS = 100 +MAX_WEIGHT_KG = 500 + + +class ScreenLocker: + """Screen locker that requires workout logging to unlock.""" + + def __init__(self, *, demo_mode: bool = True) -> None: + """Initialize screen locker with optional demo mode.""" + # Set up log file path + script_dir = os.path.dirname(os.path.abspath(__file__)) + self.log_file = os.path.join(script_dir, "workout_log.json") + + # Check if already logged today + if self.has_logged_today(): + logging.info("Workout already logged today. Skipping screen lock.") + sys.exit(0) + + self.root = tk.Tk() + self.root.title("Workout Locker" + (" [DEMO MODE]" if demo_mode else "")) + self.demo_mode = demo_mode + self.lockout_time = ( + 10 if demo_mode else 1800 + ) # 10 seconds for demo, 30 minutes for production + self.workout_data = {} + + # Get total screen dimensions across all monitors + screen_width = self.root.winfo_screenwidth() + screen_height = self.root.winfo_screenheight() + + # Override redirect to bypass window manager (needed for multi-monitor spanning) + self.root.overrideredirect(True) + + # Position window at 0,0 and span all monitors + self.root.geometry(f"{screen_width}x{screen_height}+0+0") + + # Make window fullscreen and on top + self.root.attributes("-fullscreen", True) + self.root.attributes("-topmost", True) + self.root.configure(bg="#1a1a1a", cursor="arrow") + + if demo_mode: + # Demo mode: only close button allowed + # Add close button in top-left corner + close_btn = tk.Button( + self.root, + text="✕ Close Demo", + font=("Arial", 12), + bg="#ff4444", + fg="white", + command=self.close, + cursor="hand2", + ) + close_btn.place(x=10, y=10) + + # Create main container + self.container = tk.Frame(self.root, bg="#1a1a1a") + self.container.place(relx=0.5, rely=0.5, anchor="center") + + # Start with initial question + self.ask_workout_done() + + # Force window to update and grab input after everything is set up + self.root.update_idletasks() + self.root.focus_force() + self.root.grab_set_global() + + def clear_container(self) -> None: + """Remove all widgets from the main container.""" + for widget in self.container.winfo_children(): + widget.destroy() + + def ask_workout_done(self) -> None: + """Display the initial workout question dialog.""" + self.clear_container() + + question = tk.Label( + self.container, + text="Did you work out today?", + font=("Arial", 36, "bold"), + fg="white", + bg="#1a1a1a", + ) + question.pack(pady=30) + + button_frame = tk.Frame(self.container, bg="#1a1a1a") + button_frame.pack(pady=20) + + yes_btn = tk.Button( + button_frame, + text="YES", + font=("Arial", 24, "bold"), + bg="#00aa00", + fg="white", + width=10, + command=self.ask_workout_type, + cursor="hand2" if self.demo_mode else "", + ) + yes_btn.pack(side="left", padx=20) + + no_btn = tk.Button( + button_frame, + text="NO", + font=("Arial", 24, "bold"), + bg="#aa0000", + fg="white", + width=10, + command=self.lockout, + cursor="hand2" if self.demo_mode else "", + ) + no_btn.pack(side="left", padx=20) + + def lockout(self) -> None: + """Display lockout screen with countdown timer.""" + self.clear_container() + + self.lockout_label = tk.Label( + self.container, + text=f"Go work out!\nLocked for {self.lockout_time} seconds", + font=("Arial", 48, "bold"), + fg="#ff4444", + bg="#1a1a1a", + ) + self.lockout_label.pack(pady=30) + + self.countdown_label = tk.Label( + self.container, + text=str(self.lockout_time), + font=("Arial", 120, "bold"), + fg="white", + bg="#1a1a1a", + ) + self.countdown_label.pack(pady=30) + + self.remaining_time = self.lockout_time + self.update_lockout_countdown() + + def update_lockout_countdown(self) -> None: + """Update the lockout countdown timer display.""" + if self.remaining_time > 0: + self.countdown_label.config(text=str(self.remaining_time)) + self.remaining_time -= 1 + self.root.after(1000, self.update_lockout_countdown) + else: + self.ask_workout_done() + + def ask_workout_type(self) -> None: + """Display workout type selection dialog.""" + self.clear_container() + + question = tk.Label( + self.container, + text="What type of workout?", + font=("Arial", 36, "bold"), + fg="white", + bg="#1a1a1a", + ) + question.pack(pady=30) + + button_frame = tk.Frame(self.container, bg="#1a1a1a") + button_frame.pack(pady=20) + + running_btn = tk.Button( + button_frame, + text="RUNNING", + font=("Arial", 24, "bold"), + bg="#0066cc", + fg="white", + width=15, + command=self.ask_running_details, + cursor="hand2" if self.demo_mode else "", + ) + running_btn.pack(side="left", padx=20) + + strength_btn = tk.Button( + button_frame, + text="STRENGTH", + font=("Arial", 24, "bold"), + bg="#cc6600", + fg="white", + width=15, + command=self.ask_strength_details, + cursor="hand2" if self.demo_mode else "", + ) + strength_btn.pack(side="left", padx=20) + + def ask_running_details(self) -> None: + """Display running workout input form.""" + self.clear_container() + self.workout_data["type"] = "running" + + title = tk.Label( + self.container, + text="Running Details", + font=("Arial", 36, "bold"), + fg="white", + bg="#1a1a1a", + ) + title.pack(pady=20) + + # Distance + dist_frame = tk.Frame(self.container, bg="#1a1a1a") + dist_frame.pack(pady=10) + tk.Label( + dist_frame, + text="Distance (km):", + font=("Arial", 20), + fg="white", + bg="#1a1a1a", + ).pack(side="left", padx=10) + self.distance_entry = tk.Entry(dist_frame, font=("Arial", 20), width=10) + self.distance_entry.pack(side="left", padx=10) + + # Time + time_frame = tk.Frame(self.container, bg="#1a1a1a") + time_frame.pack(pady=10) + tk.Label( + time_frame, + text="Time (minutes):", + font=("Arial", 20), + fg="white", + bg="#1a1a1a", + ).pack(side="left", padx=10) + self.time_entry = tk.Entry(time_frame, font=("Arial", 20), width=10) + self.time_entry.pack(side="left", padx=10) + + # Pace + pace_frame = tk.Frame(self.container, bg="#1a1a1a") + pace_frame.pack(pady=10) + tk.Label( + pace_frame, + text="Pace (min/km):", + font=("Arial", 20), + fg="white", + bg="#1a1a1a", + ).pack(side="left", padx=10) + self.pace_entry = tk.Entry(pace_frame, font=("Arial", 20), width=10) + self.pace_entry.pack(side="left", padx=10) + + # Timer countdown label + self.timer_label = tk.Label( + self.container, text="", font=("Arial", 16), fg="#ffaa00", bg="#1a1a1a" + ) + self.timer_label.pack(pady=10) + + self.submit_btn = tk.Button( + self.container, + text="SUBMIT (locked)", + font=("Arial", 24, "bold"), + bg="#666666", + fg="white", + width=15, + state="disabled", + cursor="hand2" if self.demo_mode else "", + ) + self.submit_btn.pack(pady=10) + + # Back button + back_btn = tk.Button( + self.container, + text="← BACK", + font=("Arial", 18), + bg="#666666", + fg="white", + width=15, + command=self.ask_workout_type, + cursor="hand2" if self.demo_mode else "", + ) + back_btn.pack(pady=10) + + # Start 30 second timer + self.submit_unlock_time = 30 + self.entries_to_check = [self.distance_entry, self.time_entry, self.pace_entry] + self.submit_command = self.verify_running_data + self.update_submit_timer() + + def verify_running_data(self) -> None: + """Validate running workout data and unlock if valid.""" + try: + distance = float(self.distance_entry.get()) + time_mins = float(self.time_entry.get()) + pace = float(self.pace_entry.get()) + + # Sanity checks + if distance <= 0 or distance > MAX_DISTANCE_KM: + self.show_error(f"Distance seems unrealistic (0-{MAX_DISTANCE_KM} km)") + return + + if time_mins <= 0 or time_mins > MAX_TIME_MINUTES: + self.show_error( + f"Time seems unrealistic (0-{MAX_TIME_MINUTES} minutes)" + ) + return + + if pace <= 0 or pace > MAX_PACE_MIN_PER_KM: + self.show_error( + f"Pace seems unrealistic (0-{MAX_PACE_MIN_PER_KM} min/km)" + ) + return + + # Calculate expected pace and check if close enough + expected_pace = time_mins / distance + pace_diff = abs(pace - expected_pace) + tolerance = expected_pace * 0.15 # 15% tolerance + + if pace_diff > tolerance: + self.show_error( + f"Pace doesn't match! " + f"Expected ~{expected_pace:.2f} min/km, got {pace:.2f}" + ) + return + + # Data looks good + self.unlock_screen() + + except ValueError: + self.show_error("Please enter valid numbers") + + def ask_strength_details(self) -> None: + """Display strength training input form.""" + self.clear_container() + self.workout_data["type"] = "strength" + + title = tk.Label( + self.container, + text="Strength Training Details", + font=("Arial", 36, "bold"), + fg="white", + bg="#1a1a1a", + ) + title.pack(pady=20) + + # Exercises + ex_frame = tk.Frame(self.container, bg="#1a1a1a") + ex_frame.pack(pady=10) + tk.Label( + ex_frame, + text="Exercises (comma-separated):", + font=("Arial", 18), + fg="white", + bg="#1a1a1a", + ).pack(side="left", padx=10) + self.exercises_entry = tk.Entry(ex_frame, font=("Arial", 18), width=30) + self.exercises_entry.pack(side="left", padx=10) + + # Sets per exercise + sets_frame = tk.Frame(self.container, bg="#1a1a1a") + sets_frame.pack(pady=10) + tk.Label( + sets_frame, + text="Sets per exercise (comma-separated):", + font=("Arial", 18), + fg="white", + bg="#1a1a1a", + ).pack(side="left", padx=10) + self.sets_entry = tk.Entry(sets_frame, font=("Arial", 18), width=20) + self.sets_entry.pack(side="left", padx=10) + + # Reps per set + reps_frame = tk.Frame(self.container, bg="#1a1a1a") + reps_frame.pack(pady=10) + tk.Label( + reps_frame, + text="Reps per set (comma-separated):", + font=("Arial", 18), + fg="white", + bg="#1a1a1a", + ).pack(side="left", padx=10) + self.reps_entry = tk.Entry(reps_frame, font=("Arial", 18), width=20) + self.reps_entry.pack(side="left", padx=10) + + # Weights + weights_frame = tk.Frame(self.container, bg="#1a1a1a") + weights_frame.pack(pady=10) + tk.Label( + weights_frame, + text="Weight per exercise in kg (comma-separated):", + font=("Arial", 18), + fg="white", + bg="#1a1a1a", + ).pack(side="left", padx=10) + self.weights_entry = tk.Entry(weights_frame, font=("Arial", 18), width=20) + self.weights_entry.pack(side="left", padx=10) + + # Total weight lifted + total_frame = tk.Frame(self.container, bg="#1a1a1a") + total_frame.pack(pady=10) + tk.Label( + total_frame, + text="Total weight lifted (kg):", + font=("Arial", 18), + fg="white", + bg="#1a1a1a", + ).pack(side="left", padx=10) + self.total_weight_entry = tk.Entry(total_frame, font=("Arial", 18), width=15) + self.total_weight_entry.pack(side="left", padx=10) + + # Timer countdown label + self.timer_label = tk.Label( + self.container, text="", font=("Arial", 16), fg="#ffaa00", bg="#1a1a1a" + ) + self.timer_label.pack(pady=10) + + self.submit_btn = tk.Button( + self.container, + text="SUBMIT (locked)", + font=("Arial", 24, "bold"), + bg="#666666", + fg="white", + width=15, + state="disabled", + cursor="hand2" if self.demo_mode else "", + ) + self.submit_btn.pack(pady=10) + + # Back button + back_btn = tk.Button( + self.container, + text="← BACK", + font=("Arial", 18), + bg="#666666", + fg="white", + width=15, + command=self.ask_workout_type, + cursor="hand2" if self.demo_mode else "", + ) + back_btn.pack(pady=10) + + # Start 30 second timer + self.submit_unlock_time = 30 + self.entries_to_check = [ + self.exercises_entry, + self.sets_entry, + self.reps_entry, + self.weights_entry, + self.total_weight_entry, + ] + self.submit_command = self.verify_strength_data + self.update_submit_timer() + + def verify_strength_data(self) -> None: + """Validate strength workout data and unlock if valid.""" + try: + exercises = [e.strip() for e in self.exercises_entry.get().split(",")] + sets = [int(s.strip()) for s in self.sets_entry.get().split(",")] + reps = [int(r.strip()) for r in self.reps_entry.get().split(",")] + weights = [float(w.strip()) for w in self.weights_entry.get().split(",")] + total_weight = float(self.total_weight_entry.get()) + + # Check all lists have same length + if not (len(exercises) == len(sets) == len(reps) == len(weights)): + self.show_error( + "Number of exercises, sets, reps, and weights must match" + ) + return + + # Check for empty or lazy entries + if any(len(ex) < MIN_EXERCISE_NAME_LEN for ex in exercises): + self.show_error("Exercise names too short - be specific") + return + + # Sanity checks + if any(s < 1 or s > MAX_SETS for s in sets): + self.show_error(f"Sets should be between 1-{MAX_SETS}") + return + + if any(r < 1 or r > MAX_REPS for r in reps): + self.show_error(f"Reps should be between 1-{MAX_REPS}") + return + + if any(w < 0 or w > MAX_WEIGHT_KG for w in weights): + self.show_error(f"Weights should be between 0-{MAX_WEIGHT_KG} kg") + return + + # Calculate expected total weight + expected_total = sum( + sets[i] * reps[i] * weights[i] for i in range(len(exercises)) + ) + weight_diff = abs(total_weight - expected_total) + tolerance = expected_total * 0.15 # 15% tolerance + + if weight_diff > tolerance: + self.show_error( + f"Total weight doesn't match! " + f"Expected ~{expected_total:.1f} kg, got {total_weight:.1f}" + ) + return + + # Data looks good + self.unlock_screen() + + except ValueError: + self.show_error("Please enter valid data in correct format") + + def update_submit_timer(self) -> None: + """Update countdown timer and check if submit can be enabled.""" + # Check if widgets still exist (user might have clicked back) + try: + if self.submit_unlock_time > 0: + self.timer_label.config( + text=f"Submit available in {self.submit_unlock_time} seconds..." + ) + self.submit_unlock_time -= 1 + self.root.after(1000, self.update_submit_timer) + else: + # Timer finished, check if all entries have data + all_filled = all(entry.get().strip() for entry in self.entries_to_check) + + if all_filled: + # Enable submit button + self.submit_btn.config( + text="SUBMIT", + state="normal", + bg="#00aa00", + command=self.submit_command, + ) + self.timer_label.config(text="You can now submit!") + else: + # Check again in 1 second + self.timer_label.config(text="Fill all fields to enable submit") + self.root.after(1000, self.check_entries_filled) + except tk.TclError: + # Widgets were destroyed (user clicked back), stop the timer + pass + + def check_entries_filled(self) -> None: + """Continuously check if entries are filled after timer expires.""" + try: + all_filled = all(entry.get().strip() for entry in self.entries_to_check) + + if all_filled: + self.submit_btn.config( + text="SUBMIT", + state="normal", + bg="#00aa00", + command=self.submit_command, + ) + self.timer_label.config(text="You can now submit!") + else: + self.timer_label.config(text="Fill all fields to enable submit") + self.root.after(1000, self.check_entries_filled) + except tk.TclError: + # Widgets were destroyed (user clicked back), stop checking + pass + + def show_error(self, message: str) -> None: + """Display error message with retry option.""" + self.clear_container() + + error_label = tk.Label( + self.container, + text="ERROR", + font=("Arial", 48, "bold"), + fg="#ff4444", + bg="#1a1a1a", + ) + error_label.pack(pady=20) + + msg_label = tk.Label( + self.container, + text=message, + font=("Arial", 24), + fg="white", + bg="#1a1a1a", + wraplength=800, + ) + msg_label.pack(pady=20) + + retry_btn = tk.Button( + self.container, + text="TRY AGAIN", + font=("Arial", 24, "bold"), + bg="#0066cc", + fg="white", + width=15, + command=self.ask_workout_done, + cursor="hand2" if self.demo_mode else "", + ) + retry_btn.pack(pady=30) + + def unlock_screen(self) -> None: + """Save workout log and display success message.""" + # Save workout data to log + self.save_workout_log() + + self.clear_container() + + success_label = tk.Label( + self.container, + text="Great job! 💪", + font=("Arial", 48, "bold"), + fg="#00ff00", + bg="#1a1a1a", + ) + success_label.pack(pady=30) + + unlock_label = tk.Label( + self.container, + text="Screen Unlocked!", + font=("Arial", 36), + fg="white", + bg="#1a1a1a", + ) + unlock_label.pack(pady=20) + + self.root.after(1500, self.close) + + def has_logged_today(self) -> bool: + """Check if workout has been logged today.""" + if not os.path.exists(self.log_file): + return False + + try: + with open(self.log_file) as f: + logs = json.load(f) + except (OSError, json.JSONDecodeError): + return False + else: + today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d") + return today in logs + + def save_workout_log(self) -> None: + """Save workout data to log file.""" + # Load existing logs + logs = {} + if os.path.exists(self.log_file): + try: + with open(self.log_file) as f: + logs = json.load(f) + except (OSError, json.JSONDecodeError): + logs = {} + + # Add today's workout + today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d") + logs[today] = { + "timestamp": datetime.now(tz=timezone.utc).isoformat(), + "workout_data": self.workout_data, + } + + # Save updated logs + try: + with open(self.log_file, "w") as f: + json.dump(logs, f, indent=2) + except OSError as e: + logging.warning(f"Could not save workout log: {e}") + + def close(self) -> None: + """Close the application and exit.""" + self.root.destroy() + sys.exit(0) + + def run(self) -> None: + """Start the Tkinter main event loop.""" + self.root.mainloop() + + +if __name__ == "__main__": + # Check for --production flag + demo_mode = True # Default to demo mode for safety + + if len(sys.argv) > 1 and sys.argv[1] == "--production": + demo_mode = False + + locker = ScreenLocker(demo_mode=demo_mode) + locker.run() diff --git a/screen_locker/workout-locker.service b/screen_locker/workout-locker.service new file mode 100644 index 0000000..f44732e --- /dev/null +++ b/screen_locker/workout-locker.service @@ -0,0 +1,14 @@ +[Unit] +Description=Workout Screen Locker +After=graphical-session.target + +[Service] +Type=simple +Environment=DISPLAY=:0 +ExecStartPre=/bin/sleep 3 +ExecStart=/usr/bin/python3 /home/kuhy/testsAndMisc/PYTHON/screen_locker/screen_lock.py +Restart=no +User=%u + +[Install] +WantedBy=graphical-session.target diff --git a/screen_locker/workout_log.json b/screen_locker/workout_log.json new file mode 100644 index 0000000..7b624cc --- /dev/null +++ b/screen_locker/workout_log.json @@ -0,0 +1,20 @@ +{ + "2025-11-27": { + "timestamp": "2025-11-27T19:45:38.894904", + "workout_data": { + "type": "strength" + } + }, + "2025-11-28": { + "timestamp": "2025-11-28T12:46:09.077724", + "workout_data": { + "type": "strength" + } + }, + "2025-11-30": { + "timestamp": "2025-11-30T12:12:44.884093", + "workout_data": { + "type": "strength" + } + } +}