mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-07-04 13:23:13 +02:00
feat: sick mode
This commit is contained in:
parent
7b3bca7c78
commit
9da4456237
87
screen_locker/adjust_shutdown_schedule.sh
Executable file
87
screen_locker/adjust_shutdown_schedule.sh
Executable file
@ -0,0 +1,87 @@
|
||||
#!/bin/bash
|
||||
# Helper script to adjust shutdown schedule
|
||||
# This script should be allowed via sudoers for the workout locker
|
||||
#
|
||||
# Usage: sudo adjust_shutdown_schedule.sh [--restore] <mon_wed_hour> <thu_sun_hour> <morning_end_hour>
|
||||
#
|
||||
# --restore: Allow restoring to original (possibly later) times
|
||||
# Without this flag, only stricter (earlier) times are allowed
|
||||
#
|
||||
# Add to /etc/sudoers.d/workout-locker:
|
||||
# <username> ALL=(root) NOPASSWD: /home/kuhy/testsAndMisc/python_pkg/screen_locker/adjust_shutdown_schedule.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CONFIG_FILE="/etc/shutdown-schedule.conf"
|
||||
CANONICAL_FILE="/usr/local/share/locked-shutdown-schedule.conf"
|
||||
|
||||
# Check for --restore flag
|
||||
RESTORE_MODE=false
|
||||
if [[ "${1:-}" == "--restore" ]]; then
|
||||
RESTORE_MODE=true
|
||||
shift
|
||||
fi
|
||||
|
||||
# Validate arguments
|
||||
if [[ $# -ne 3 ]]; then
|
||||
echo "Usage: $0 [--restore] <mon_wed_hour> <thu_sun_hour> <morning_end_hour>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MON_WED_HOUR="$1"
|
||||
THU_SUN_HOUR="$2"
|
||||
MORNING_END_HOUR="$3"
|
||||
|
||||
# Validate hours are integers between 0-23
|
||||
for hour in "$MON_WED_HOUR" "$THU_SUN_HOUR" "$MORNING_END_HOUR"; do
|
||||
if ! [[ "$hour" =~ ^[0-9]+$ ]] || [[ "$hour" -lt 0 ]] || [[ "$hour" -gt 23 ]]; then
|
||||
echo "Error: Hours must be integers between 0 and 23" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Read current values to check if we're making schedule stricter
|
||||
if [[ -f "$CONFIG_FILE" ]] && [[ "$RESTORE_MODE" == false ]]; then
|
||||
# shellcheck source=/dev/null
|
||||
source "$CONFIG_FILE" 2>/dev/null || true
|
||||
OLD_MON_WED="${MON_WED_HOUR:-24}"
|
||||
OLD_THU_SUN="${THU_SUN_HOUR:-24}"
|
||||
|
||||
# Reset variables to new values for comparison
|
||||
# shellcheck disable=SC2034
|
||||
MON_WED_HOUR_OLD="$OLD_MON_WED"
|
||||
# shellcheck disable=SC2034
|
||||
THU_SUN_HOUR_OLD="$OLD_THU_SUN"
|
||||
|
||||
# Only allow making schedule stricter (earlier shutdown) unless in restore mode
|
||||
if [[ "$1" -gt "${MON_WED_HOUR_OLD:-24}" ]] || [[ "$2" -gt "${THU_SUN_HOUR_OLD:-24}" ]]; then
|
||||
echo "Error: Can only make schedule stricter (earlier shutdown times)" >&2
|
||||
echo "Use --restore flag to restore original times" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
NEW_CONFIG="# Shutdown schedule configuration
|
||||
# Modified by screen_locker sick day feature at $(date)
|
||||
MON_WED_HOUR=$1
|
||||
THU_SUN_HOUR=$2
|
||||
MORNING_END_HOUR=$3
|
||||
"
|
||||
|
||||
# Remove immutable attributes
|
||||
chattr -i "$CONFIG_FILE" 2>/dev/null || true
|
||||
chattr -i "$CANONICAL_FILE" 2>/dev/null || true
|
||||
|
||||
# Write new config
|
||||
echo "$NEW_CONFIG" > "$CONFIG_FILE"
|
||||
echo "$NEW_CONFIG" > "$CANONICAL_FILE"
|
||||
|
||||
# Set permissions
|
||||
chmod 644 "$CONFIG_FILE"
|
||||
chmod 644 "$CANONICAL_FILE"
|
||||
|
||||
# Re-apply immutable attributes
|
||||
chattr +i "$CONFIG_FILE" || echo "Warning: Could not set immutable on $CONFIG_FILE" >&2
|
||||
chattr +i "$CANONICAL_FILE" || echo "Warning: Could not set immutable on $CANONICAL_FILE" >&2
|
||||
|
||||
echo "Shutdown schedule updated: Mon-Wed=${1}:00, Thu-Sun=${2}:00, Morning end=${3}:00"
|
||||
@ -8,6 +8,7 @@ from datetime import datetime, timezone
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tkinter as tk
|
||||
|
||||
@ -21,6 +22,12 @@ MIN_EXERCISE_NAME_LEN = 3
|
||||
MAX_SETS = 20
|
||||
MAX_REPS = 100
|
||||
MAX_WEIGHT_KG = 500
|
||||
SICK_LOCKOUT_SECONDS = 120 # 2 minutes wait when sick
|
||||
SHUTDOWN_CONFIG_FILE = Path("/etc/shutdown-schedule.conf")
|
||||
# Helper script path (relative to this file)
|
||||
ADJUST_SHUTDOWN_SCRIPT = Path(__file__).resolve().parent / "adjust_shutdown_schedule.sh"
|
||||
# State file to track sick day usage and original config values
|
||||
SICK_DAY_STATE_FILE = Path(__file__).resolve().parent / "sick_day_state.json"
|
||||
|
||||
|
||||
class ScreenLocker:
|
||||
@ -126,11 +133,332 @@ class ScreenLocker:
|
||||
bg="#aa0000",
|
||||
fg="white",
|
||||
width=10,
|
||||
command=self.ask_if_sick,
|
||||
cursor="hand2" if self.demo_mode else "",
|
||||
)
|
||||
no_btn.pack(side="left", padx=20)
|
||||
|
||||
def ask_if_sick(self) -> None:
|
||||
"""Display sick day question dialog."""
|
||||
self.clear_container()
|
||||
|
||||
question = tk.Label(
|
||||
self.container,
|
||||
text="Are you sick?",
|
||||
font=("Arial", 36, "bold"),
|
||||
fg="white",
|
||||
bg="#1a1a1a",
|
||||
)
|
||||
question.pack(pady=30)
|
||||
|
||||
info_label = tk.Label(
|
||||
self.container,
|
||||
text="If yes, shutdown time will be moved 1.5 hours earlier",
|
||||
font=("Arial", 18),
|
||||
fg="#ffaa00",
|
||||
bg="#1a1a1a",
|
||||
)
|
||||
info_label.pack(pady=10)
|
||||
|
||||
button_frame = tk.Frame(self.container, bg="#1a1a1a")
|
||||
button_frame.pack(pady=20)
|
||||
|
||||
yes_btn = tk.Button(
|
||||
button_frame,
|
||||
text="YES (sick)",
|
||||
font=("Arial", 24, "bold"),
|
||||
bg="#cc6600",
|
||||
fg="white",
|
||||
width=12,
|
||||
command=self.handle_sick_day,
|
||||
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=12,
|
||||
command=self.lockout,
|
||||
cursor="hand2" if self.demo_mode else "",
|
||||
)
|
||||
no_btn.pack(side="left", padx=20)
|
||||
|
||||
def handle_sick_day(self) -> None:
|
||||
"""Handle sick day: adjust shutdown time and start 2-minute wait."""
|
||||
self.clear_container()
|
||||
|
||||
# Check if sick mode was already used today (time already adjusted)
|
||||
already_adjusted_today = self._sick_mode_used_today()
|
||||
|
||||
if already_adjusted_today:
|
||||
# Already adjusted today, just show status and proceed to wait
|
||||
status_text = "Shutdown time already adjusted today"
|
||||
status_color = "#ffaa00"
|
||||
else:
|
||||
# First sick mode use today - adjust the shutdown time
|
||||
adjustment_success = self._adjust_shutdown_time_earlier()
|
||||
|
||||
if adjustment_success:
|
||||
status_text = (
|
||||
"Shutdown time moved 1.5 hours earlier ✓\n(Will revert tomorrow)"
|
||||
)
|
||||
status_color = "#00aa00"
|
||||
else:
|
||||
status_text = "Could not adjust shutdown time (check permissions)"
|
||||
status_color = "#ff4444"
|
||||
|
||||
title = tk.Label(
|
||||
self.container,
|
||||
text="Sick Day Mode",
|
||||
font=("Arial", 36, "bold"),
|
||||
fg="#cc6600",
|
||||
bg="#1a1a1a",
|
||||
)
|
||||
title.pack(pady=20)
|
||||
|
||||
status_label = tk.Label(
|
||||
self.container,
|
||||
text=status_text,
|
||||
font=("Arial", 18),
|
||||
fg=status_color,
|
||||
bg="#1a1a1a",
|
||||
)
|
||||
status_label.pack(pady=10)
|
||||
|
||||
wait_label = tk.Label(
|
||||
self.container,
|
||||
text="Please wait 2 minutes before unlocking...",
|
||||
font=("Arial", 24),
|
||||
fg="white",
|
||||
bg="#1a1a1a",
|
||||
)
|
||||
wait_label.pack(pady=20)
|
||||
|
||||
self.sick_countdown_label = tk.Label(
|
||||
self.container,
|
||||
text=str(SICK_LOCKOUT_SECONDS),
|
||||
font=("Arial", 80, "bold"),
|
||||
fg="white",
|
||||
bg="#1a1a1a",
|
||||
)
|
||||
self.sick_countdown_label.pack(pady=30)
|
||||
|
||||
self.sick_remaining_time = SICK_LOCKOUT_SECONDS
|
||||
self._update_sick_countdown()
|
||||
|
||||
def _update_sick_countdown(self) -> None:
|
||||
"""Update the sick day countdown timer."""
|
||||
if self.sick_remaining_time > 0:
|
||||
self.sick_countdown_label.config(text=str(self.sick_remaining_time))
|
||||
self.sick_remaining_time -= 1
|
||||
self.root.after(1000, self._update_sick_countdown)
|
||||
else:
|
||||
# Record sick day and unlock
|
||||
self.workout_data["type"] = "sick_day"
|
||||
self.workout_data["note"] = "Sick day - shutdown moved earlier"
|
||||
self.unlock_screen()
|
||||
|
||||
def _adjust_shutdown_time_earlier(self) -> bool:
|
||||
"""Adjust shutdown schedule 1.5 hours earlier (stricter).
|
||||
|
||||
This can only be used once per day. Original values are saved and
|
||||
automatically restored when checked the next day.
|
||||
|
||||
Returns True if successful, False otherwise.
|
||||
"""
|
||||
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
|
||||
|
||||
# Restore original values if there's a state from a previous day
|
||||
self._restore_original_config_if_needed()
|
||||
|
||||
# Check if sick mode was already used today (after potential restore)
|
||||
if self._sick_mode_used_today():
|
||||
_logger.warning("Sick mode already used today")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Read current config
|
||||
config_values = self._read_shutdown_config()
|
||||
if config_values is None:
|
||||
return False
|
||||
|
||||
mon_wed_hour, thu_sun_hour, morning_end_hour = config_values
|
||||
|
||||
# Save original values FIRST before any modification
|
||||
if not self._save_sick_day_state(today, mon_wed_hour, thu_sun_hour):
|
||||
_logger.error("Failed to save state - aborting adjustment")
|
||||
return False
|
||||
|
||||
# Move shutdown times 1 hour earlier
|
||||
new_mon_wed = mon_wed_hour - 1
|
||||
new_thu_sun = thu_sun_hour - 1
|
||||
|
||||
# Ensure we don't go below reasonable hours (e.g., not before 18:00)
|
||||
new_mon_wed = max(18, new_mon_wed)
|
||||
new_thu_sun = max(18, new_thu_sun)
|
||||
|
||||
# Write new config
|
||||
return self._write_shutdown_config(
|
||||
new_mon_wed, new_thu_sun, morning_end_hour
|
||||
)
|
||||
|
||||
except (OSError, ValueError) as e:
|
||||
_logger.warning("Failed to adjust shutdown time: %s", e)
|
||||
return False
|
||||
|
||||
def _sick_mode_used_today(self) -> bool:
|
||||
"""Check if sick mode was already used today."""
|
||||
if not SICK_DAY_STATE_FILE.exists():
|
||||
return False
|
||||
|
||||
try:
|
||||
with SICK_DAY_STATE_FILE.open() as f:
|
||||
state = json.load(f)
|
||||
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
|
||||
return state.get("date") == today
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return False
|
||||
|
||||
def _save_sick_day_state(
|
||||
self, date: str, orig_mon_wed: int, orig_thu_sun: int
|
||||
) -> bool:
|
||||
"""Save sick day state with original config values.
|
||||
|
||||
Returns True if saved successfully, False otherwise.
|
||||
"""
|
||||
state = {
|
||||
"date": date,
|
||||
"original_mon_wed_hour": orig_mon_wed,
|
||||
"original_thu_sun_hour": orig_thu_sun,
|
||||
}
|
||||
try:
|
||||
with SICK_DAY_STATE_FILE.open("w") as f:
|
||||
json.dump(state, f, indent=2)
|
||||
except OSError as e:
|
||||
_logger.warning("Failed to save sick day state: %s", e)
|
||||
return False
|
||||
|
||||
_logger.info("Saved sick day state for %s", date)
|
||||
return True
|
||||
|
||||
def _restore_original_config_if_needed(self) -> None:
|
||||
"""Restore original config values if sick day state is from a previous day."""
|
||||
if not SICK_DAY_STATE_FILE.exists():
|
||||
return
|
||||
|
||||
try:
|
||||
with SICK_DAY_STATE_FILE.open() as f:
|
||||
state = json.load(f)
|
||||
|
||||
state_date = state.get("date")
|
||||
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
|
||||
|
||||
# Only restore if state is from a previous day
|
||||
if state_date and state_date != today:
|
||||
orig_mon_wed = state.get("original_mon_wed_hour")
|
||||
orig_thu_sun = state.get("original_thu_sun_hour")
|
||||
|
||||
if orig_mon_wed is not None and orig_thu_sun is not None:
|
||||
# Read current morning end hour
|
||||
config_values = self._read_shutdown_config()
|
||||
if config_values:
|
||||
_, _, morning_end_hour = config_values
|
||||
_logger.info(
|
||||
"Restoring original shutdown config from %s", state_date
|
||||
)
|
||||
self._write_shutdown_config(
|
||||
orig_mon_wed, orig_thu_sun, morning_end_hour, restore=True
|
||||
)
|
||||
|
||||
# Remove stale state file
|
||||
SICK_DAY_STATE_FILE.unlink()
|
||||
_logger.info("Removed stale sick day state from %s", state_date)
|
||||
|
||||
except (OSError, json.JSONDecodeError) as e:
|
||||
_logger.warning("Error checking sick day state: %s", e)
|
||||
|
||||
def _read_shutdown_config(self) -> tuple[int, int, int] | None:
|
||||
"""Read current shutdown config values.
|
||||
|
||||
Returns tuple of (mon_wed_hour, thu_sun_hour, morning_end_hour) or None.
|
||||
"""
|
||||
if not SHUTDOWN_CONFIG_FILE.exists():
|
||||
_logger.warning("Shutdown config file not found: %s", SHUTDOWN_CONFIG_FILE)
|
||||
return None
|
||||
|
||||
mon_wed_hour = None
|
||||
thu_sun_hour = None
|
||||
morning_end_hour = None
|
||||
|
||||
with SHUTDOWN_CONFIG_FILE.open() as f:
|
||||
for config_line in f:
|
||||
stripped_line = config_line.strip()
|
||||
if stripped_line.startswith("MON_WED_HOUR="):
|
||||
mon_wed_hour = int(stripped_line.split("=")[1])
|
||||
elif stripped_line.startswith("THU_SUN_HOUR="):
|
||||
thu_sun_hour = int(stripped_line.split("=")[1])
|
||||
elif stripped_line.startswith("MORNING_END_HOUR="):
|
||||
morning_end_hour = int(stripped_line.split("=")[1])
|
||||
|
||||
if mon_wed_hour is None or thu_sun_hour is None or morning_end_hour is None:
|
||||
_logger.warning("Shutdown config missing required values")
|
||||
return None
|
||||
|
||||
return (mon_wed_hour, thu_sun_hour, morning_end_hour)
|
||||
|
||||
def _write_shutdown_config(
|
||||
self,
|
||||
mon_wed_hour: int,
|
||||
thu_sun_hour: int,
|
||||
morning_end_hour: int,
|
||||
*,
|
||||
restore: bool = False,
|
||||
) -> bool:
|
||||
"""Write new shutdown config values using helper script.
|
||||
|
||||
Args:
|
||||
mon_wed_hour: Shutdown hour for Monday-Wednesday.
|
||||
thu_sun_hour: Shutdown hour for Thursday-Sunday.
|
||||
morning_end_hour: Morning end hour.
|
||||
restore: If True, allows restoring to later times (for reverting sick day).
|
||||
|
||||
Returns True if successful, False otherwise.
|
||||
"""
|
||||
if not ADJUST_SHUTDOWN_SCRIPT.exists():
|
||||
_logger.warning(
|
||||
"Adjust shutdown script not found: %s", ADJUST_SHUTDOWN_SCRIPT
|
||||
)
|
||||
return False
|
||||
|
||||
cmd = ["/usr/bin/sudo", str(ADJUST_SHUTDOWN_SCRIPT)]
|
||||
if restore:
|
||||
cmd.append("--restore")
|
||||
cmd.extend([str(mon_wed_hour), str(thu_sun_hour), str(morning_end_hour)])
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
except subprocess.SubprocessError as e:
|
||||
_logger.warning("Failed to adjust shutdown config: %s", e)
|
||||
return False
|
||||
|
||||
_logger.info(
|
||||
"Adjusted shutdown hours: Mon-Wed=%d, Thu-Sun=%d. Output: %s",
|
||||
mon_wed_hour,
|
||||
thu_sun_hour,
|
||||
result.stdout.strip(),
|
||||
)
|
||||
return True
|
||||
return True
|
||||
|
||||
def lockout(self) -> None:
|
||||
"""Display lockout screen with countdown timer."""
|
||||
self.clear_container()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user