Add install_core_system.sh; remove pc_startup and thesis_tracker

- Add linux_configuration/scripts/single_use/install_core_system.sh:
  unified installer for core modules (workout locker, hosts, shutdown
  timer) plus optional secondary modules (steam enforcer, pacman
  wrapper, i3 config, compulsive block, focus daemon)
- git rm pc_startup_visual_status.sh, setup_pc_startup_monitor.sh,
  thesis_work_tracker.sh, thesis_work_status.sh,
  setup_thesis_work_tracker.sh, README_THESIS_TRACKER.md,
  systemd/thesis-work-tracker@.service, and their two test files
- Remove now-dead setup_pc_startup_monitor.sh call from fresh-install/main.sh
This commit is contained in:
Krzysztof kuhy Rudnicki 2026-05-15 01:13:28 +02:00
parent db6276b3ff
commit 765fa92543
12 changed files with 192 additions and 2675 deletions

View File

@ -0,0 +1,33 @@
{
"intent": "Provide a single entry-point installer for core modules and clean up failed/unused scripts.",
"scope": [
"linux_configuration/scripts/single_use/install_core_system.sh — new file",
"linux_configuration/scripts/single_use/fresh-install/main.sh — remove dead call",
"Removed: pc_startup_visual_status.sh, setup_pc_startup_monitor.sh, thesis_work_tracker.sh, thesis_work_status.sh, setup_thesis_work_tracker.sh, README_THESIS_TRACKER.md, systemd/thesis-work-tracker@.service, test_thesis_work_tracker.sh, test_thesis_work_status.sh"
],
"changes": [
"Added install_core_system.sh: installs workout screen locker, hosts blocking, and midnight shutdown timer unconditionally; prompts for steam enforcer, pacman wrapper, i3 config, compulsive block, focus daemon (--all/--none flags bypass prompts)",
"Removed 9 files: pc_startup_visual_status.sh and its setup script (failed expectations), all thesis_work_tracker artefacts (failed expectations) including systemd unit and 2 test files",
"Removed dead setup_pc_startup_monitor.sh call from fresh-install/main.sh"
],
"verification": [
{
"command": "pre-commit run --all-files",
"result": "pass",
"evidence": "All 35 active hooks passed (shellcheck, codespell, block-polling-antipatterns, etc.)"
},
{
"command": "shellcheck linux_configuration/scripts/single_use/install_core_system.sh",
"result": "pass",
"evidence": "No shellcheck warnings"
}
],
"risks": [
"fresh-install/main.sh still references setup_pc_startup_monitor.sh path in comments — none present, removal was clean",
"install_core_system.sh calls sub-installers that may require sudo interactively at runtime"
],
"rollback": [
"git revert HEAD to restore removed files and undo main.sh change",
"Verify pre-commit still passes after revert"
]
}

View File

@ -1,337 +0,0 @@
# Bachelor/Master's Thesis Work Tracker
A comprehensive system to help you stay focused on your thesis by blocking distractions until you've put in your work hours.
> **Note**: This tracker was originally requested for a bachelor thesis, but works equally well for master's thesis work. The default repository name `praca_magisterska` is Polish for "master's thesis" - you can customize this during installation.
## Overview
This system monitors your active windows and tracks time spent on thesis-related work. Steam and other distracting websites are blocked until you accumulate the required work time. It's designed to be as hard to circumvent as possible while remaining fair and transparent.
## How It Works
1. **Work Tracking**: The system monitors your active window every 5 seconds
2. **Time Accumulation**: When you're working on approved thesis applications, time accumulates
3. **Unlocking**: After reaching the work quota (default: 2 hours), distractions are unblocked
4. **Decay System**: Using Steam or distractions decays your work time (default: 30 minutes per hour)
5. **Re-blocking**: When work time falls below quota, distractions are blocked again
## Tracked Applications
The following applications count as "thesis work":
### Game Engines
- **Unreal Engine** (all versions: UE4, UE5, UnrealEditor)
- **Unity Engine** (Unity Editor and Unity Hub)
- **Nvidia Omniverse** (Omniverse and Kit)
### Development Tools
- **Visual Studio Code** - **ONLY** when working on the `praca_magisterska` repository
- The window title must contain the repository name
- Or the workspace must have the repository open
## Blocked Sites
When you haven't met your work quota, the following are blocked via `/etc/hosts`:
### Gaming
- All Steam domains (steampowered.com, steamcommunity.com, etc.)
### Social Media
- Reddit
- Twitter/X
- Facebook
- Instagram
### Video/Entertainment
- YouTube
- Twitch
- 9gag
- Imgur
## Installation
### Quick Start
```bash
# Clone or navigate to the repository
cd /path/to/scripts
# Run the installer (will prompt for sudo)
sudo scripts/periodic_background/digital_wellbeing/setup_thesis_work_tracker.sh
```
### Custom Configuration
```bash
# Set custom work quota (e.g., 3 hours)
sudo scripts/periodic_background/digital_wellbeing/setup_thesis_work_tracker.sh --work-quota 180
# Set custom decay rate (e.g., 20 minutes per hour)
sudo scripts/periodic_background/digital_wellbeing/setup_thesis_work_tracker.sh --decay-rate 20
# Set custom VS Code repository name
sudo scripts/periodic_background/digital_wellbeing/setup_thesis_work_tracker.sh --vscode-repo "my-thesis-repo"
# Combine multiple options
sudo scripts/periodic_background/digital_wellbeing/setup_thesis_work_tracker.sh \
--work-quota 150 \
--decay-rate 25 \
--vscode-repo "bachelor-thesis"
```
### Prerequisites
The installer will check for required dependencies:
- `xdotool` - for window detection
- `systemd` - for service management
On Arch Linux:
```bash
sudo pacman -S xdotool
```
On Ubuntu/Debian:
```bash
sudo apt install xdotool
```
## Usage
### After Installation
The system runs automatically as a systemd service. Just start working on your thesis!
### Checking Your Progress
```bash
# View current status
systemctl status thesis-work-tracker@$USER.service
# View live logs
tail -f /var/log/thesis-work-tracker/tracker.log
# Check your accumulated work time
sudo cat /var/lib/thesis-work-tracker/work-time.state
```
### Understanding the State File
The state file shows:
- `TOTAL_WORK_SECONDS`: Your accumulated work time (in seconds)
- `STEAM_ACCESS_GRANTED`: Whether distractions are currently unblocked (1=yes, 0=no)
- `CURRENT_SESSION_SECONDS`: Time in your current work session
- `LAST_WORK_SESSION_START`: When your current session started
### Managing the Service
```bash
# Restart the service
sudo systemctl restart thesis-work-tracker@$USER.service
# Stop the service temporarily
sudo systemctl stop thesis-work-tracker@$USER.service
# Start the service
sudo systemctl start thesis-work-tracker@$USER.service
# Disable auto-start
sudo systemctl disable thesis-work-tracker@$USER.service
# Re-enable auto-start
sudo systemctl enable thesis-work-tracker@$USER.service
```
## Uninstallation
```bash
sudo scripts/periodic_background/digital_wellbeing/setup_thesis_work_tracker.sh --uninstall
```
**Note**: This preserves your state file and logs. To completely remove everything:
```bash
# Remove state directory
sudo chattr -i -R /var/lib/thesis-work-tracker
sudo rm -rf /var/lib/thesis-work-tracker
# Remove logs
sudo rm -rf /var/log/thesis-work-tracker
```
## Security & Anti-Circumvention Features
This system is designed to be difficult to bypass:
### 1. **Immutable State Files**
- State files are protected with `chattr +i` (immutable flag)
- Cannot be edited even by root without removing the flag first
- Automatically re-applied after each update
### 2. **Auto-Restart Service**
- Systemd service automatically restarts if killed
- Runs continuously in the background
- Starts automatically on boot
### 3. **Hosts File Integration**
- Integrates with the repository's hosts guard system
- Uses immutable `/etc/hosts` file
- Cannot be easily bypassed by changing DNS
### 4. **Process Integrity**
- Monitors actual active windows, not just running processes
- Detects if you switch away from work applications
- VS Code requires specific repository to be open
### 5. **Decay Mechanism**
- Using Steam/distractions consumes your earned work time
- Forces sustained work habits, not just one-time work sessions
- Fair: 30 minutes of decay per hour of distraction usage
### 6. **Locked Configuration**
- Configuration is embedded in the installed script
- Cannot be easily modified without reinstalling
- Protected script location in `/usr/local/bin`
## Troubleshooting
### Service Not Starting
```bash
# Check service status
systemctl status thesis-work-tracker@$USER.service
# Check for errors
journalctl -u thesis-work-tracker@$USER.service -n 50
# Verify dependencies
which xdotool
which systemctl
```
### Window Detection Not Working
The tracker requires X11 and `xdotool`. Check:
```bash
# Verify X11 is running
echo $DISPLAY
# Test xdotool
xdotool getactivewindow getwindowname
# Check XAUTHORITY
echo $XAUTHORITY
ls -la ~/.Xauthority
```
### VS Code Repository Not Detected
Make sure:
1. The window title shows the repository name
2. You're working in the correct repository folder
3. The repository name matches what you specified during installation
Test with:
```bash
xdotool getactivewindow getwindowname
# Should show something like: "praca_magisterska - Visual Studio Code"
```
### Hosts File Not Updating
Check:
```bash
# View current hosts file
sudo cat /etc/hosts | grep steam
# Check immutable flag
lsattr /etc/hosts
# Service logs
tail -f /var/log/thesis-work-tracker/tracker.log
```
## Configuration Files
- **Tracker Script**: `/usr/local/bin/thesis_work_tracker.sh`
- **Systemd Service**: `/etc/systemd/system/thesis-work-tracker@.service`
- **State File**: `/var/lib/thesis-work-tracker/work-time.state`
- **Log File**: `/var/log/thesis-work-tracker/tracker.log`
## Tips for Success
1. **Start Early**: Begin your work sessions in the morning when you're fresh
2. **Take Breaks**: The system only tracks active window time, so take regular breaks
3. **Focus Sessions**: Work in focused 2-hour blocks to unlock entertainment
4. **Monitor Progress**: Check your logs regularly to see your work patterns
5. **Be Honest**: The system trusts you're actually working when applications are open
## FAQ
### Can I bypass this system?
Technically yes, but it's designed to make bypassing more effort than just doing the work:
- You'd need to disable the service (but it auto-restarts)
- You'd need to modify immutable files (requires chattr commands)
- You'd need to fake window activity (complex)
- You'd need to edit protected state files (also complex)
The point isn't to make it impossible, but to add enough friction that doing your thesis work is easier.
### What if I need to use VS Code for something else?
VS Code only counts as work when you're in the `praca_magisterska` repository. Other projects won't count toward your thesis time.
### Can I adjust the work quota after installation?
Yes, but you need to:
1. Uninstall the current system
2. Reinstall with new parameters
3. Your accumulated time is preserved in the state file
### Does this work on Wayland?
Currently, this requires X11 for `xdotool` window detection. Wayland support would require adapting to use different tools like `wlrctl` or `swaymsg`.
### What happens if I reboot?
The service starts automatically on boot, and your accumulated work time is preserved in the state file.
## License
This is part of the kuhyx/scripts repository. Use at your own risk and discretion.
## Contributing
Found a bug or have a suggestion? Please open an issue in the main repository.
## Acknowledgments
This tool is built on top of the digital wellbeing framework in this repository, including:
- Hosts guard system
- Psychological friction mechanisms
- Systemd service patterns
Good luck with your bachelor thesis! 🎓

View File

@ -1,286 +0,0 @@
#!/bin/bash
# Visual PC Startup Monitor Status Display
# Shows a nice visual representation of the monitoring status and schedule
# Color codes for visual display
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
WHITE='\033[1;37m'
GRAY='\033[0;37m'
NC='\033[0m' # No Color
# Unicode symbols for visual elements
CHECK="✓"
CROSS="✗"
WARNING="⚠️"
CLOCK="🕐"
CALENDAR="📅"
COMPUTER="💻"
BELL="🔔"
# Function to draw a box around text
draw_box() {
local text="$1"
local width=${#text}
local padding=2
local total_width=$((width + padding * 2))
# Top border
printf "┌"
printf "─%.0s" $(seq 1 $total_width)
printf "┐\n"
# Content with padding
printf "│%*s%s%*s│\n" $padding "" "$text" $padding ""
# Bottom border
printf "└"
printf "─%.0s" $(seq 1 $total_width)
printf "┘\n"
}
# Function to show current day status
show_day_status() {
local day_of_week
day_of_week=$(date +%u)
printf '%s%s Day Status%s\n' "$BLUE" "$CALENDAR" "$NC"
printf '═══════════════\n'
# Show all days with status
local days=("Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday" "Sunday")
local monitored=(1 0 0 0 1 1 1) # 1=monitored, 0=not monitored
for i in {0..6}; do
local day_num=$((i + 1))
if [[ $day_num -eq 7 ]]; then day_num=0; fi # Sunday is 0 in some contexts
if [[ ${monitored[$i]} -eq 1 ]]; then
if [[ $day_of_week -eq $((i + 1)) ]] || [[ $day_of_week -eq 7 && $i -eq 6 ]]; then
printf '%s%s %s (TODAY - MONITORED)%s\n' "$GREEN" "$CHECK" "${days[$i]}" "$NC"
else
printf '%s%s %s (monitored)%s\n' "$CYAN" "$CHECK" "${days[$i]}" "$NC"
fi
else
if [[ $day_of_week -eq $((i + 1)) ]]; then
printf '%s○ %s (TODAY - not monitored)%s\n' "$GRAY" "${days[$i]}" "$NC"
else
printf '%s○ %s%s\n' "$GRAY" "${days[$i]}" "$NC"
fi
fi
done
printf "\n"
}
# Function to show time window status
show_time_status() {
local current_hour current_minute current_hour_num
current_hour=$(date +%H)
current_minute=$(date +%M)
current_hour_num=$((10#$current_hour))
printf '%s%s Time Window Status%s\n' "$YELLOW" "$CLOCK" "$NC"
printf '═══════════════════════\n'
# Show 24-hour timeline with window highlighted
printf 'Timeline (24-hour format):\n'
printf '00 01 02 03 04 '
printf '%s05 06 07%s ' "$GREEN" "$NC"
printf '08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23\n'
printf ' '
printf '%s▲─────▲%s\n' "$GREEN" "$NC"
printf ' '
printf '%sExpected Window%s\n' "$GREEN" "$NC"
# Current time indicator
printf '\nCurrent time: %s%02d:%s%s\n' "$WHITE" "$current_hour_num" "$current_minute" "$NC"
if [[ $current_hour_num -ge 5 && $current_hour_num -lt 8 ]]; then
printf 'Status: %s%s Within expected window (5AM-8AM)%s\n' "$GREEN" "$CHECK" "$NC"
else
printf 'Status: %s○ Outside expected window%s\n' "$YELLOW" "$NC"
fi
printf '\n'
}
# Function to show boot time analysis
show_boot_analysis() {
printf '%s%s Boot Time Analysis%s\n' "$PURPLE" "$COMPUTER" "$NC"
printf '═══════════════════════\n'
# Get boot time
local uptime_seconds boot_time boot_date boot_time_only boot_hour boot_hour_num today
uptime_seconds=$(awk '{print int($1)}' /proc/uptime 2> /dev/null || echo "0")
boot_time=$(date -d "@$(($(date +%s) - uptime_seconds))" +"%Y-%m-%d %H:%M:%S")
boot_date=$(echo "$boot_time" | cut -d' ' -f1)
boot_time_only=$(echo "$boot_time" | cut -d' ' -f2)
boot_hour=$(echo "$boot_time_only" | cut -d':' -f1)
boot_hour_num=$((10#$boot_hour))
today=$(date +%Y-%m-%d)
printf 'System boot time: %s%s%s\n' "$WHITE" "$boot_time" "$NC"
if [[ $boot_date == "$today" ]]; then
printf 'Boot date: %s%s Today%s\n' "$GREEN" "$CHECK" "$NC"
if [[ $boot_hour_num -ge 5 && $boot_hour_num -lt 8 ]]; then
printf 'Boot window: %s%s Within expected window (5AM-8AM)%s\n' "$GREEN" "$CHECK" "$NC"
printf 'Status: %s%s COMPLIANT%s\n' "$GREEN" "$CHECK" "$NC"
else
printf 'Boot window: %s%s Outside expected window%s\n' "$RED" "$CROSS" "$NC"
printf 'Status: %s%s NON-COMPLIANT%s\n' "$RED" "$WARNING" "$NC"
fi
else
printf 'Boot date: %s○ Not today (%s)%s\n' "$YELLOW" "$boot_date" "$NC"
printf 'Status: %s○ System was not booted today%s\n' "$YELLOW" "$NC"
fi
printf '\n'
}
# Function to show monitoring system status
show_system_status() {
printf '%s%s Monitoring System%s\n' "$CYAN" "$BELL" "$NC"
printf '═══════════════════════\n'
# Check if timer exists and is enabled
if systemctl is-enabled pc-startup-monitor.timer &> /dev/null; then
printf 'Service: %s%s ENABLED%s\n' "$GREEN" "$CHECK" "$NC"
if systemctl is-active pc-startup-monitor.timer &> /dev/null; then
printf 'Timer: %s%s ACTIVE%s\n' "$GREEN" "$CHECK" "$NC"
else
printf 'Timer: %s%s INACTIVE%s\n' "$RED" "$CROSS" "$NC"
fi
# Show next check time
local next_check
next_check=$(systemctl list-timers pc-startup-monitor.timer --no-pager 2> /dev/null | grep pc-startup-monitor | awk '{print $1, $2, $3}' || echo "Not scheduled")
printf 'Next check: %s%s%s\n' "$WHITE" "$next_check" "$NC"
else
printf 'Service: %s%s NOT ENABLED%s\n' "$RED" "$CROSS" "$NC"
printf 'Timer: %s%s NOT ACTIVE%s\n' "$RED" "$CROSS" "$NC"
fi
printf '\n'
}
# Function to show overall compliance status
show_compliance_overview() {
local day_of_week current_hour current_hour_num
day_of_week=$(date +%u)
current_hour=$(date +%H)
current_hour_num=$((10#$current_hour))
# Check if today is monitored
local is_monitored=false
if [[ $day_of_week == "1" ]] || [[ $day_of_week == "5" ]] || [[ $day_of_week == "6" ]] || [[ $day_of_week == "7" ]]; then
is_monitored=true
fi
printf '%s' "$WHITE"
draw_box "COMPLIANCE OVERVIEW"
printf '%s\n' "$NC"
if [[ $is_monitored == true ]]; then
printf 'Today: %s%s Monitored day%s\n' "$GREEN" "$CHECK" "$NC"
# Check current compliance
if [[ $current_hour_num -ge 5 && $current_hour_num -lt 8 ]]; then
printf 'Current status: %s%s PC is on during expected window%s\n' "$GREEN" "$CHECK" "$NC"
printf 'Action needed: %sNone - currently compliant%s\n' "$GREEN" "$NC"
else
# Check if booted in window
local uptime_seconds boot_time boot_date boot_hour boot_hour_num today
uptime_seconds=$(awk '{print int($1)}' /proc/uptime 2> /dev/null || echo "0")
boot_time=$(date -d "@$(($(date +%s) - uptime_seconds))" +"%Y-%m-%d %H:%M:%S")
boot_date=$(echo "$boot_time" | cut -d' ' -f1)
boot_hour=$(echo "$boot_time" | cut -d' ' -f2 | cut -d':' -f1)
boot_hour_num=$((10#$boot_hour))
today=$(date +%Y-%m-%d)
if [[ $boot_date == "$today" ]] && [[ $boot_hour_num -ge 5 && $boot_hour_num -lt 8 ]]; then
printf 'Current status: %s%s PC was booted in expected window%s\n' "$GREEN" "$CHECK" "$NC"
printf 'Action needed: %sNone - compliant%s\n' "$GREEN" "$NC"
else
printf 'Current status: %s%s PC was NOT booted in expected window%s\n' "$RED" "$WARNING" "$NC"
printf 'Action needed: %sWarning will be shown at 8:30 AM%s\n' "$YELLOW" "$NC"
fi
fi
else
printf 'Today: %s○ Not a monitored day%s\n' "$GRAY" "$NC"
printf 'Current status: %sNo monitoring required%s\n' "$GRAY" "$NC"
printf 'Action needed: %sNone%s\n' "$GRAY" "$NC"
fi
printf '\n'
}
# Function to show recent activity
show_recent_activity() {
printf '%s📋 Recent Activity%s\n' "$GRAY" "$NC"
printf '════════════════\n'
# Show last 5 log entries
local logs
logs=$(journalctl -t pc-startup-monitor --no-pager -n 5 --output=short 2> /dev/null || echo "No logs found")
if [[ $logs == "No logs found" ]]; then
printf '%sNo recent monitoring activity%s\n' "$GRAY" "$NC"
else
echo "$logs" | while IFS= read -r line; do
if [[ $line == *"WARNING"* ]]; then
printf '%s%s%s\n' "$RED" "$line" "$NC"
elif [[ $line == *"compliance OK"* ]]; then
printf '%s%s%s\n' "$GREEN" "$line" "$NC"
else
printf '%s%s%s\n' "$GRAY" "$line" "$NC"
fi
done
fi
printf '\n'
}
# Main display function
main() {
clear
# Header
printf '%s' "$BLUE"
draw_box "PC STARTUP MONITOR - VISUAL STATUS"
printf '%s\n\n' "$NC"
local current_datetime system_uptime
current_datetime=$(date)
system_uptime=$(uptime -p)
printf '%sCurrent Date/Time: %s%s\n' "$WHITE" "$current_datetime" "$NC"
printf '%sSystem Uptime: %s%s\n\n' "$WHITE" "$system_uptime" "$NC"
# Show all status sections
show_day_status
show_time_status
show_boot_analysis
show_system_status
show_compliance_overview
show_recent_activity
# Footer with commands
printf '%s═══════════════════════════════════════════════════════════════%s\n' "$BLUE" "$NC"
printf '%sCommands:%s\n' "$WHITE" "$NC"
printf ' %s%s%s - Show system status\n' "$CYAN" "sudo pc-startup-monitor-manager.sh status" "$NC"
printf ' %s%s%s - Test monitor now\n' "$CYAN" "sudo pc-startup-monitor-manager.sh test" "$NC"
printf ' %s%s%s - View detailed logs\n' "$CYAN" "sudo pc-startup-monitor-manager.sh logs" "$NC"
printf ' %s%s%s - Show this visual status\n' "$CYAN" "$0" "$NC"
printf '%s═══════════════════════════════════════════════════════════════%s\n' "$BLUE" "$NC"
}
# Run main function
main "$@"

View File

@ -1,502 +0,0 @@
#!/bin/bash
# Script to monitor PC startup times on specific days
# Checks if PC was turned on between 5AM-8AM on Monday, Friday, Saturday, Sunday
# Handles sudo privileges automatically
set -e # Exit on any error
# Source common library for shared functions
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../../lib/common.sh
source "$SCRIPT_DIR/../../lib/common.sh"
# Parse interactive/help arguments
parse_interactive_args "$@"
shift "$COMMON_ARGS_SHIFT"
echo "PC Startup Time Monitor for Arch Linux"
echo "======================================"
echo "Current Date: $(get_datetime)"
echo "User: $(get_actual_user)"
if [[ $INTERACTIVE_MODE == "true" ]]; then
echo "Mode: Interactive (prompts enabled)"
else
echo "Mode: Automatic (auto-yes, use --interactive for prompts)"
fi
# Get the actual user (even when running with sudo)
ACTUAL_USER="$(get_actual_user)"
USER_HOME="$(get_actual_user_home)"
echo "Target user: $ACTUAL_USER"
echo "User home: $USER_HOME"
# Function to check if today is a monitored day
is_monitored_day() {
is_day_of_week 1 5 6 7 # 1=Monday, 5=Friday, 6=Saturday, 7=Sunday
}
# Function to check if current time is between 5AM and 8AM
is_current_time_in_window() {
is_hour_in_range 5 8
}
# Function to check if PC was booted between 5AM-8AM today
was_booted_in_window_today() {
local boot_datetime boot_date boot_hour boot_hour_num today
today=$(get_date)
boot_datetime=$(get_boot_datetime)
echo "Boot time detected: $boot_datetime"
# Check if boot time is from today
boot_date=$(echo "$boot_datetime" | cut -d' ' -f1)
if [[ $boot_date != "$today" ]]; then
echo "PC was not booted today (boot date: $boot_date, today: $today)"
return 1 # Not booted today
fi
# Extract hour from boot time
boot_hour=$(echo "$boot_datetime" | cut -d' ' -f2 | cut -d':' -f1)
boot_hour_num=$((10#$boot_hour)) # Convert to decimal
echo "Boot hour: $boot_hour_num"
# Check if boot time was between 5AM (5) and 8AM (8, before 8AM)
if [[ $boot_hour_num -ge 5 ]] && [[ $boot_hour_num -lt 8 ]]; then
echo "PC was booted in the expected window (5AM-8AM)"
return 0 # Yes, booted in window
else
echo "PC was NOT booted in the expected window (5AM-8AM)"
return 1 # No, not booted in window
fi
}
# Function to show notification/warning
show_startup_warning() {
local day_name current_time today
day_name=$(get_day_name)
current_time=$(printf '%(%H:%M)T' -1)
today=$(get_date)
echo ""
echo "⚠️ PC STARTUP TIME WARNING"
echo "=========================="
echo "Date: $today ($day_name)"
echo "Current time: $current_time"
echo ""
echo "This PC was expected to be turned on between 5:00 AM and 8:00 AM today,"
echo "but it was not turned on during that time window."
echo ""
echo "Expected: Monday, Friday, Saturday, Sunday between 5:00-8:00 AM"
echo "Actual: PC was turned on outside the expected window"
echo ""
# Log the warning
logger -t pc-startup-monitor "WARNING: PC was not turned on during expected window (5AM-8AM) on $day_name $today"
# Try to show desktop notification if possible
if command -v notify-send &>/dev/null && [[ -n $DISPLAY ]]; then
if [[ $EUID -eq 0 ]]; then
# Running as root, send notification as user
sudo -u "$ACTUAL_USER" DISPLAY="$DISPLAY" notify-send "PC Startup Warning" "PC was not turned on between 5AM-8AM as expected on $day_name" --urgency=normal --expire-time=10000 2>/dev/null || true
else
notify-send "PC Startup Warning" "PC was not turned on between 5AM-8AM as expected on $day_name" --urgency=normal --expire-time=10000 2>/dev/null || true
fi
fi
echo "This warning has been logged to the system journal."
echo "You can view startup logs with: journalctl -t pc-startup-monitor"
echo ""
}
# Function to create the monitoring service
create_monitoring_service() {
echo ""
echo "1. Creating PC Startup Monitor Service..."
echo "======================================="
local service_file="/etc/systemd/system/pc-startup-monitor.service"
cat >"$service_file" <<'EOF'
[Unit]
Description=PC Startup Time Monitor
After=multi-user.target
Wants=network.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/pc-startup-check.sh
StandardOutput=journal
StandardError=journal
RemainAfterExit=true
[Install]
WantedBy=multi-user.target
EOF
echo "✓ Created monitoring service: $service_file"
}
# Function to create the monitoring timer
create_monitoring_timer() {
echo ""
echo "2. Creating PC Startup Monitor Timer..."
echo "====================================="
local timer_file="/etc/systemd/system/pc-startup-monitor.timer"
cat >"$timer_file" <<'EOF'
[Unit]
Description=Timer for PC startup monitoring
Requires=pc-startup-monitor.service
[Timer]
OnCalendar=*-*-* 08:30:00
Persistent=false
AccuracySec=1m
[Install]
WantedBy=timers.target
EOF
echo "✓ Created monitoring timer: $timer_file"
}
# Function to create the main monitoring script
create_monitoring_script() {
echo ""
echo "3. Creating PC Startup Monitor Script..."
echo "======================================"
local script_file="/usr/local/bin/pc-startup-check.sh"
cat >"$script_file" <<'EOF'
#!/bin/bash
# PC Startup Time Monitor Check Script
# Monitors if PC was turned on during expected hours on specific days
# Function to check if today is a monitored day
is_monitored_day() {
local day_of_week
day_of_week=$(date +%u) # 1=Monday, 7=Sunday
# Check if today is Monday (1), Friday (5), Saturday (6), or Sunday (7)
if [[ "$day_of_week" == "1" ]] || [[ "$day_of_week" == "5" ]] || [[ "$day_of_week" == "6" ]] || [[ "$day_of_week" == "7" ]]; then
return 0 # Yes, it's a monitored day
else
return 1 # No, it's not a monitored day
fi
}
# Function to check if current time is between 5AM and 8AM
is_current_time_in_window() {
local current_hour current_hour_num
current_hour=$(date +%H)
current_hour_num=$((10#$current_hour))
if [[ $current_hour_num -ge 5 ]] && [[ $current_hour_num -lt 8 ]]; then
return 0 # Yes, current time is in the 5AM-8AM window
else
return 1 # No, current time is outside the window
fi
}
# Function to check if PC was booted between 5AM-8AM today
was_booted_in_window_today() {
local today boot_time
today=$(date +%Y-%m-%d)
# Calculate boot time from uptime
local uptime_seconds
uptime_seconds=$(awk '{print int($1)}' /proc/uptime 2>/dev/null || echo "0")
boot_time=$(date -d "@$(($(date +%s) - uptime_seconds))" +"%Y-%m-%d %H:%M:%S")
# Check if boot time is from today
local boot_date
boot_date=$(echo "$boot_time" | cut -d' ' -f1)
if [[ "$boot_date" != "$today" ]]; then
return 1 # Not booted today
fi
# Extract hour from boot time
local boot_hour boot_hour_num
boot_hour=$(echo "$boot_time" | cut -d' ' -f2 | cut -d':' -f1)
boot_hour_num=$((10#$boot_hour))
# Check if boot time was between 5AM and 8AM
if [[ $boot_hour_num -ge 5 ]] && [[ $boot_hour_num -lt 8 ]]; then
return 0 # Yes, booted in window
else
return 1 # No, not booted in window
fi
}
# Function to show notification/warning
show_startup_warning() {
local day_name current_time today
day_name=$(date +%A)
current_time=$(date +"%H:%M")
today=$(date +%Y-%m-%d)
echo "⚠️ PC STARTUP TIME WARNING"
echo "Date: $today ($day_name)"
echo "Current time: $current_time"
echo "This PC was expected to be turned on between 5:00 AM and 8:00 AM today, but was not."
# Log the warning
logger -t pc-startup-monitor "WARNING: PC was not turned on during expected window (5AM-8AM) on $day_name $today"
}
# Main logic
echo "$(date): PC Startup Monitor Check"
logger -t pc-startup-monitor "Running startup time check at $(date)"
# Step 0: Check if today is a monitored day
if ! is_monitored_day; then
day_name=$(date +%A)
echo "$(date): Today is $day_name - not a monitored day. Skipping check."
logger -t pc-startup-monitor "Skipping check - today ($day_name) is not a monitored day"
exit 0
fi
# Step 1 & 2: Check if current time is between 5AM and 8AM
if is_current_time_in_window; then
echo "$(date): Current time is within 5AM-8AM window. No action needed."
logger -t pc-startup-monitor "Current time is within monitored window (5AM-8AM) - no action needed"
exit 0
fi
# Step 4: Check if PC was turned on between 5AM-8AM today
if was_booted_in_window_today; then
echo "$(date): PC was booted in expected window (5AM-8AM). All good."
logger -t pc-startup-monitor "PC was booted in expected window (5AM-8AM) - compliance OK"
else
echo "$(date): PC was NOT booted in expected window (5AM-8AM). Showing warning."
show_startup_warning
fi
EOF
chmod +x "$script_file"
echo "✓ Created monitoring script: $script_file"
}
# Function to create management script
create_management_script() {
echo ""
echo "4. Creating Management Script..."
echo "=============================="
local script_file="/usr/local/bin/pc-startup-monitor-manager.sh"
cat >"$script_file" <<'EOF'
#!/bin/bash
# PC Startup Monitor Manager
# Provides easy management of the PC startup monitoring feature
TIMER_NAME="pc-startup-monitor.timer"
SERVICE_NAME="pc-startup-monitor.service"
show_status() {
echo "PC Startup Monitor Status"
echo "========================"
if systemctl is-enabled "$TIMER_NAME" &>/dev/null; then
echo "Status: ENABLED"
if systemctl is-active "$TIMER_NAME" &>/dev/null; then
echo "Timer: ACTIVE"
else
echo "Timer: INACTIVE"
fi
else
echo "Status: NOT ENABLED"
fi
echo ""
echo "Next check scheduled:"
systemctl list-timers "$TIMER_NAME" --no-pager 2>/dev/null | grep "$TIMER_NAME" || echo "Timer not active"
echo ""
echo "Recent logs:"
journalctl -t pc-startup-monitor --no-pager -n 10 2>/dev/null || echo "No recent logs"
}
test_now() {
echo "Running startup monitor check now..."
/usr/local/bin/pc-startup-check.sh
}
case "$1" in
"status")
show_status
;;
"logs")
echo "PC Startup Monitor Logs"
echo "======================"
journalctl -t pc-startup-monitor --no-pager -n 30
;;
"test")
test_now
;;
*)
echo "PC Startup Monitor Manager"
echo "Usage: $0 {status|logs|test}"
echo ""
echo "Commands:"
echo " status - Show current status and next check time"
echo " logs - Show recent monitoring logs"
echo " test - Run a startup check now (for testing)"
echo ""
show_status
;;
esac
EOF
chmod +x "$script_file"
echo "✓ Created management script: $script_file"
}
# Function to enable the services
enable_services() {
echo ""
echo "5. Enabling PC Startup Monitor..."
echo "==============================="
# Reload systemd daemon
systemctl daemon-reload
echo "✓ Reloaded systemd daemon"
# Enable and start the timer
systemctl enable pc-startup-monitor.timer
echo "✓ Enabled pc-startup-monitor timer"
systemctl start pc-startup-monitor.timer
echo "✓ Started pc-startup-monitor timer"
}
# Function to test the setup
test_setup() {
echo ""
echo "6. Testing Setup..."
echo "=================="
echo "Service files:"
if [[ -f "/etc/systemd/system/pc-startup-monitor.service" ]]; then
echo "✓ Service file exists"
else
echo "✗ Service file missing"
fi
if [[ -f "/etc/systemd/system/pc-startup-monitor.timer" ]]; then
echo "✓ Timer file exists"
else
echo "✗ Timer file missing"
fi
echo ""
echo "Timer status:"
if systemctl is-enabled pc-startup-monitor.timer &>/dev/null; then
echo "✓ Timer is enabled"
else
echo "✗ Timer is not enabled"
fi
if systemctl is-active pc-startup-monitor.timer &>/dev/null; then
echo "✓ Timer is active"
else
echo "✗ Timer is not active"
fi
echo ""
echo "Testing current logic:"
/usr/local/bin/pc-startup-check.sh
}
# Function to show final instructions
show_instructions() {
echo ""
echo "=========================================="
echo "PC Startup Monitor Setup Complete"
echo "=========================================="
echo "Summary:"
echo "✓ Monitoring service created (/etc/systemd/system/pc-startup-monitor.service)"
echo "✓ Monitoring timer created (/etc/systemd/system/pc-startup-monitor.timer)"
echo "✓ Monitor script created (/usr/local/bin/pc-startup-check.sh)"
echo "✓ Management script created (/usr/local/bin/pc-startup-monitor-manager.sh)"
echo "✓ Timer enabled and started"
echo ""
echo "How it works:"
echo "• Monitors PC startup times on Monday, Friday, Saturday, Sunday"
echo "• Expects PC to be turned on between 5:00 AM - 8:00 AM"
echo "• Checks daily at 8:30 AM if PC was turned on in expected window"
echo "• Shows warning if PC was not turned on during expected time"
echo ""
echo "Management commands:"
echo " sudo pc-startup-monitor-manager.sh status - Check status"
echo " sudo pc-startup-monitor-manager.sh logs - View monitor logs"
echo " sudo pc-startup-monitor-manager.sh test - Test monitor now"
echo ""
echo "Next check: Tomorrow at 8:30 AM (if it's a monitored day)"
echo ""
}
# Function to prompt for confirmation
confirm_setup() {
echo ""
echo "PC Startup Monitor Setup"
echo "======================="
echo "This will set up monitoring for PC startup times."
echo ""
echo "Monitoring schedule:"
echo "- Days: Monday, Friday, Saturday, Sunday"
echo "- Expected startup time: 5:00 AM - 8:00 AM"
echo "- Check time: 8:30 AM daily"
echo "- Action: Show warning if PC wasn't started in expected window"
echo ""
if [[ $INTERACTIVE_MODE == "true" ]]; then
read -r -p "Do you want to proceed? (y/N): " confirm
case "$confirm" in
[yY] | [yY][eE][sS])
echo "Proceeding with setup..."
return 0
;;
*)
echo "Setup cancelled."
exit 0
;;
esac
else
echo "Auto-proceeding with setup (use --interactive to prompt)"
echo "Proceeding with setup..."
return 0
fi
}
# Main execution flow
main() {
# Check for sudo privileges
check_sudo "$@"
# Confirm setup
confirm_setup
# Create all components
create_monitoring_service
create_monitoring_timer
create_monitoring_script
create_management_script
# Enable services
enable_services
# Test setup
test_setup
# Show instructions
show_instructions
}
# Run main function
main "$@"

View File

@ -1,365 +0,0 @@
#!/bin/bash
# Bachelor Thesis Work Tracker - One-Shot Installer
#
# This script installs a system that:
# 1. Monitors active windows for thesis-related work (Unreal Engine, Unity, Nvidia Omniverse, VS Code with specific repo)
# 2. Tracks accumulated work time with protection against tampering
# 3. Blocks Steam and other distractions via /etc/hosts until work quota is met
# 4. Provides psychological friction against circumvention
#
# The system is designed to be as hard to circumvent as possible:
# - State files are immutable (chattr +i)
# - Runs as a systemd service that auto-restarts
# - Integrated with hosts guard system
# - Protected against easy time manipulation
#
# Usage:
# sudo ./setup_thesis_work_tracker.sh [options]
#
# Options:
# --work-quota MINUTES Set required work time in minutes (default: 120 = 2 hours)
# --decay-rate MINUTES Set decay rate per hour of distraction usage (default: 30)
# --vscode-repo NAME Set required VS Code repository name (default: praca_magisterska)
# --dry-run Show what would be done without making changes
# --uninstall Remove the thesis work tracker system
# -h|--help Show this help
#
# Exit codes:
# 0 = success
# 1 = general failure
# 2 = argument error
set -euo pipefail
######################################################################
# Configuration Defaults
######################################################################
WORK_QUOTA_MINUTES=120 # 2 hours of work required
DECAY_RATE_MINUTES=30 # Lose 30 minutes per hour of Steam usage
VSCODE_REPO="praca_magisterska"
DRY_RUN=0
UNINSTALL=0
######################################################################
# Paths
######################################################################
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
TRACKER_SCRIPT="$SCRIPT_DIR/thesis_work_tracker.sh"
STATUS_SCRIPT="$SCRIPT_DIR/thesis_work_status.sh"
SERVICE_FILE="$SCRIPT_DIR/systemd/thesis-work-tracker@.service"
INSTALL_BIN="/usr/local/bin/thesis_work_tracker.sh"
INSTALL_STATUS="/usr/local/bin/thesis_work_status"
INSTALL_SERVICE="/etc/systemd/system/thesis-work-tracker@.service"
STATE_DIR="/var/lib/thesis-work-tracker"
LOG_DIR="/var/log/thesis-work-tracker"
######################################################################
# Colors and Logging
######################################################################
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m' # No Color
msg() { printf "${GREEN}[+]${NC} %s\n" "$*"; }
note() { printf "${BLUE}[i]${NC} %s\n" "$*"; }
warn() { printf "${YELLOW}[!]${NC} %s\n" "$*"; }
err() { printf "${RED}[x]${NC} %s\n" "$*" >&2; }
run() {
if [[ $DRY_RUN -eq 1 ]]; then
printf '%s[DRY-RUN]%s ' "${CYAN}" "${NC}"
printf '%q ' "$@"
printf '\n'
else
"$@"
fi
}
######################################################################
# Helpers
######################################################################
require_root() {
if [[ $EUID -ne 0 ]]; then
exec sudo -E bash "$0" "$@"
fi
}
usage() {
head -n 31 "$0" | tail -n +2 | sed 's/^# \{0,1\}//'
}
check_dependencies() {
local missing=()
for cmd in xdotool systemctl; do
if ! command -v "$cmd" &>/dev/null; then
missing+=("$cmd")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
err "Missing required dependencies: ${missing[*]}"
note "Install them with: sudo pacman -S ${missing[*]}"
return 1
fi
}
get_current_user() {
# Get the user who invoked sudo, or current user if not using sudo
if [[ -n ${SUDO_USER:-} ]]; then
echo "$SUDO_USER"
else
whoami
fi
}
######################################################################
# Parse Arguments
######################################################################
while [[ $# -gt 0 ]]; do
case "$1" in
--work-quota)
WORK_QUOTA_MINUTES="${2:-}"
[[ -z $WORK_QUOTA_MINUTES ]] && {
err "--work-quota requires a value"
exit 2
}
if ! [[ $WORK_QUOTA_MINUTES =~ ^[0-9]+$ ]] || [[ $WORK_QUOTA_MINUTES -le 0 ]]; then
err "--work-quota must be a positive integer (got: $WORK_QUOTA_MINUTES)"
exit 2
fi
shift 2
;;
--decay-rate)
DECAY_RATE_MINUTES="${2:-}"
[[ -z $DECAY_RATE_MINUTES ]] && {
err "--decay-rate requires a value"
exit 2
}
if ! [[ $DECAY_RATE_MINUTES =~ ^[0-9]+$ ]] || [[ $DECAY_RATE_MINUTES -lt 0 ]]; then
err "--decay-rate must be a non-negative integer (got: $DECAY_RATE_MINUTES)"
exit 2
fi
shift 2
;;
--vscode-repo)
VSCODE_REPO="${2:-}"
[[ -z $VSCODE_REPO ]] && {
err "--vscode-repo requires a value"
exit 2
}
shift 2
;;
--dry-run)
DRY_RUN=1
shift
;;
--uninstall)
UNINSTALL=1
shift
;;
-h | --help)
usage
exit 0
;;
*)
err "Unknown option: $1"
usage
exit 2
;;
esac
done
######################################################################
# Main Functions
######################################################################
uninstall_tracker() {
msg "Uninstalling thesis work tracker..."
# Get current user for service name
local user
user=$(get_current_user)
# Stop and disable service
if systemctl is-active --quiet "thesis-work-tracker@$user.service" 2>/dev/null; then
run systemctl stop "thesis-work-tracker@$user.service"
fi
if systemctl is-enabled --quiet "thesis-work-tracker@$user.service" 2>/dev/null; then
run systemctl disable "thesis-work-tracker@$user.service"
fi
# Remove service file
if [[ -f $INSTALL_SERVICE ]]; then
run rm -f "$INSTALL_SERVICE"
run systemctl daemon-reload
fi
# Remove tracker script
if [[ -f $INSTALL_BIN ]]; then
run rm -f "$INSTALL_BIN"
fi
# Remove status script
if [[ -f $INSTALL_STATUS ]]; then
run rm -f "$INSTALL_STATUS"
fi
# Remove state directory (with immutable flags removed)
if [[ -d $STATE_DIR ]]; then
run chattr -i -R "$STATE_DIR" 2>/dev/null || true
note "State directory preserved at: $STATE_DIR"
note "To completely remove state: sudo rm -rf $STATE_DIR"
fi
msg "Thesis work tracker uninstalled successfully"
note "Log files preserved at: $LOG_DIR"
}
install_tracker() {
msg "Installing thesis work tracker..."
# Check dependencies
check_dependencies || exit 1
# Verify source files exist
if [[ ! -f $TRACKER_SCRIPT ]]; then
err "Tracker script not found: $TRACKER_SCRIPT"
exit 1
fi
if [[ ! -f $STATUS_SCRIPT ]]; then
err "Status script not found: $STATUS_SCRIPT"
exit 1
fi
if [[ ! -f $SERVICE_FILE ]]; then
err "Service file not found: $SERVICE_FILE"
exit 1
fi
# Create directories
msg "Creating directories..."
run mkdir -p "$LOG_DIR"
run chmod 755 "$LOG_DIR"
# Install tracker script with configuration
msg "Installing tracker script to $INSTALL_BIN..."
# Copy script and update configuration values
run cp "$TRACKER_SCRIPT" "$INSTALL_BIN"
# Update configuration in the installed script
local work_quota_seconds=$((WORK_QUOTA_MINUTES * 60))
local decay_rate_seconds=$((DECAY_RATE_MINUTES * 60))
run sed -i "s/^WORK_QUOTA_REQUIRED=.*/WORK_QUOTA_REQUIRED=$work_quota_seconds # $WORK_QUOTA_MINUTES minutes/" "$INSTALL_BIN"
run sed -i "s/^WORK_DECAY_PER_HOUR=.*/WORK_DECAY_PER_HOUR=$decay_rate_seconds # $DECAY_RATE_MINUTES minutes/" "$INSTALL_BIN"
run sed -i "s/^VSCODE_REQUIRED_REPO=.*/VSCODE_REQUIRED_REPO=\"$VSCODE_REPO\"/" "$INSTALL_BIN"
run chmod 755 "$INSTALL_BIN"
# Install status script
msg "Installing status script to $INSTALL_STATUS..."
run cp "$STATUS_SCRIPT" "$INSTALL_STATUS"
# Update quota in status script to match
run sed -i "s/^WORK_QUOTA_REQUIRED=.*/WORK_QUOTA_REQUIRED=$work_quota_seconds # $WORK_QUOTA_MINUTES minutes/" "$INSTALL_STATUS"
run chmod 755 "$INSTALL_STATUS"
# Install systemd service
msg "Installing systemd service..."
run cp "$SERVICE_FILE" "$INSTALL_SERVICE"
run chmod 644 "$INSTALL_SERVICE"
run systemctl daemon-reload
# Get current user for service enablement
local user
user=$(get_current_user)
# Enable and start service
msg "Enabling and starting service for user: $user..."
run systemctl enable "thesis-work-tracker@$user.service"
run systemctl restart "thesis-work-tracker@$user.service"
# Wait a moment for service to start
sleep 2
# Check service status
if systemctl is-active --quiet "thesis-work-tracker@$user.service"; then
msg "Service started successfully!"
else
warn "Service may not have started properly. Check status with:"
warn " systemctl status thesis-work-tracker@$user.service"
fi
# Display configuration summary
echo ""
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ Bachelor Thesis Work Tracker - Installation ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
echo "Configuration:"
echo " • Work quota required: ${BOLD}${WORK_QUOTA_MINUTES} minutes${NC}"
echo " • Decay rate (per hour of Steam): ${BOLD}${DECAY_RATE_MINUTES} minutes${NC}"
echo " • VS Code repository: ${BOLD}${VSCODE_REPO}${NC}"
echo ""
echo "Tracked Applications:"
echo " ✓ Unreal Engine (all versions)"
echo " ✓ Unity Editor"
echo " ✓ Nvidia Omniverse"
echo " ✓ Visual Studio Code (only when working on '$VSCODE_REPO')"
echo ""
echo "Blocked Sites (until quota met):"
echo " ⛔ Steam (all domains)"
echo " ⛔ Social media (Reddit, Twitter, Facebook, Instagram)"
echo " ⛔ Video sites (YouTube, Twitch)"
echo " ⛔ Other distractions (9gag, Imgur)"
echo ""
echo "System Protection Features:"
echo " 🔒 State files protected with immutable flags"
echo " 🔒 Auto-restart on failure"
echo " 🔒 Integrated with hosts guard system"
echo " 🔒 Continuous monitoring every 5 seconds"
echo ""
echo "How it works:"
echo " 1. Work on your thesis using the approved applications"
echo " 2. Time accumulates in the background"
echo " 3. After ${WORK_QUOTA_MINUTES} minutes of work, Steam is unblocked"
echo " 4. Steam usage decays your work time at ${DECAY_RATE_MINUTES} min/hour"
echo " 5. When work time drops below quota, Steam is blocked again"
echo ""
echo "Useful Commands:"
echo " • Check progress: thesis_work_status"
echo " • Check status: systemctl status thesis-work-tracker@$user.service"
echo " • View logs: tail -f $LOG_DIR/tracker.log"
echo " • View state: sudo cat $STATE_DIR/work-time.state"
echo " • Restart: sudo systemctl restart thesis-work-tracker@$user.service"
echo " • Uninstall: sudo $0 --uninstall"
echo ""
echo "⚠️ IMPORTANT: This system is designed to be hard to circumvent!"
echo " State files are immutable and the service auto-restarts."
echo " To legitimately modify settings, uninstall and reinstall."
echo ""
echo "Good luck with your bachelor thesis! 🎓"
echo ""
}
######################################################################
# Main
######################################################################
require_root "$@"
if [[ $UNINSTALL -eq 1 ]]; then
uninstall_tracker
else
install_tracker
fi
exit 0

View File

@ -1,29 +0,0 @@
[Unit]
Description=Bachelor Thesis Work Tracker
Documentation=man:systemd.service(5)
After=graphical.target
Wants=graphical.target
[Service]
Type=simple
ExecStart=/usr/local/bin/thesis_work_tracker.sh
Restart=always
RestartSec=10
# Run as the user who is logged in (for X11/window detection)
User=%i
Environment="DISPLAY=:0"
Environment="XAUTHORITY=/home/%i/.Xauthority"
# Logging
StandardOutput=append:/var/log/thesis-work-tracker/tracker.log
StandardError=append:/var/log/thesis-work-tracker/tracker.log
# Security hardening
NoNewPrivileges=false
PrivateTmp=true
ProtectSystem=false
ProtectHome=false
[Install]
WantedBy=default.target

View File

@ -1,159 +0,0 @@
#!/bin/bash
# Quick status checker for thesis work tracker
# Shows current work progress and access status
set -euo pipefail
STATE_FILE="${STATE_FILE:-/var/lib/thesis-work-tracker/work-time.state}"
# Colors
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
BOLD='\033[1m'
NC='\033[0m'
# Check if state file exists
if [[ ! -f $STATE_FILE ]]; then
echo -e "${RED}Error:${NC} Thesis work tracker is not installed or has not been initialized."
echo "Install with: sudo scripts/periodic_background/digital_wellbeing/setup_thesis_work_tracker.sh"
exit 1
fi
# Load state (need sudo to read immutable file)
if [[ -z ${THESIS_STATUS_SKIP_SUDO:-} ]] && [[ $EUID -ne 0 ]]; then
exec sudo -E bash "$0" "$@"
fi
# Temporarily remove immutable to read
sudo chattr -i "$STATE_FILE" 2>/dev/null || true
# Parse state file with a single while-read pass (no grep/cut forks)
TOTAL_WORK_SECONDS=0
STEAM_ACCESS_GRANTED=0
CURRENT_SESSION_SECONDS=0
LAST_WORK_SESSION_START=0
local_key=''
local_value=''
while IFS='=' read -r local_key local_value; do
case $local_key in
TOTAL_WORK_SECONDS) TOTAL_WORK_SECONDS=$local_value ;;
STEAM_ACCESS_GRANTED) STEAM_ACCESS_GRANTED=$local_value ;;
CURRENT_SESSION_SECONDS) CURRENT_SESSION_SECONDS=$local_value ;;
LAST_WORK_SESSION_START) LAST_WORK_SESSION_START=$local_value ;;
esac
done < "$STATE_FILE" 2>/dev/null || true
# Validate that values are numeric
if ! [[ $TOTAL_WORK_SECONDS =~ ^[0-9]+$ ]]; then TOTAL_WORK_SECONDS=0; fi
if ! [[ $STEAM_ACCESS_GRANTED =~ ^[01]$ ]]; then STEAM_ACCESS_GRANTED=0; fi
if ! [[ $CURRENT_SESSION_SECONDS =~ ^[0-9]+$ ]]; then CURRENT_SESSION_SECONDS=0; fi
if ! [[ $LAST_WORK_SESSION_START =~ ^[0-9]+$ ]]; then LAST_WORK_SESSION_START=0; fi
# Re-apply immutable
sudo chattr +i "$STATE_FILE" 2>/dev/null || true
# Test mode: skip display and return to caller
if [[ -n ${THESIS_STATUS_SKIP_OUTPUT:-} ]]; then
return 0 2>/dev/null || exit 0
fi
# Default values if not set
TOTAL_WORK_SECONDS=${TOTAL_WORK_SECONDS:-0}
STEAM_ACCESS_GRANTED=${STEAM_ACCESS_GRANTED:-0}
CURRENT_SESSION_SECONDS=${CURRENT_SESSION_SECONDS:-0}
LAST_WORK_SESSION_START=${LAST_WORK_SESSION_START:-0}
# Constants (should match tracker script)
WORK_QUOTA_REQUIRED=7200 # 2 hours default
# Calculate values
work_minutes=$((TOTAL_WORK_SECONDS / 60))
work_hours=$((work_minutes / 60))
work_remaining_minutes=$((work_minutes % 60))
quota_minutes=$((WORK_QUOTA_REQUIRED / 60))
quota_hours=$((quota_minutes / 60))
quota_remaining_minutes=$((quota_minutes % 60))
remaining_seconds=$((WORK_QUOTA_REQUIRED - TOTAL_WORK_SECONDS))
if [[ $remaining_seconds -lt 0 ]]; then
remaining_seconds=0
fi
remaining_minutes=$((remaining_seconds / 60))
session_minutes=$((CURRENT_SESSION_SECONDS / 60))
percentage=$((TOTAL_WORK_SECONDS * 100 / WORK_QUOTA_REQUIRED))
if [[ $percentage -gt 100 ]]; then
percentage=100
fi
# Display status
echo ""
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ Bachelor Thesis Work Tracker - Status ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
# Work progress
echo -e "${BOLD}Work Progress:${NC}"
echo -e " Total work time: ${GREEN}${work_hours}h ${work_remaining_minutes}m${NC}"
echo -e " Required quota: ${BLUE}${quota_hours}h ${quota_remaining_minutes}m${NC}"
# Progress bar
echo -n " Progress: ["
bar_length=40
filled=$((percentage * bar_length / 100))
for ((i=0; i<bar_length; i++)); do
if [[ $i -lt $filled ]]; then
echo -n "█"
else
echo -n "░"
fi
done
echo -e "] ${percentage}%"
# Remaining time
if [[ $remaining_minutes -gt 0 ]]; then
echo -e " ${YELLOW}Need ${remaining_minutes} more minutes to unlock distractions${NC}"
else
echo -e " ${GREEN}✓ Quota met! Keep up the good work!${NC}"
fi
echo ""
# Access status
echo -e "${BOLD}Access Status:${NC}"
if [[ $STEAM_ACCESS_GRANTED -eq 1 ]]; then
echo -e " Steam & Distractions: ${GREEN}UNLOCKED${NC}"
else
echo -e " Steam & Distractions: ${RED}BLOCKED${NC}"
fi
echo ""
# Current session
if [[ $LAST_WORK_SESSION_START -ne 0 ]]; then
echo -e "${BOLD}Current Session:${NC}"
echo -e " ${GREEN}Active work session in progress${NC}"
echo -e " Session duration: ${session_minutes} minutes"
echo ""
fi
# Service status
echo -e "${BOLD}Service Status:${NC}"
if systemctl is-active --quiet "thesis-work-tracker@$(logname).service" 2>/dev/null; then
echo -e " Tracker daemon: ${GREEN}RUNNING${NC}"
else
echo -e " Tracker daemon: ${RED}NOT RUNNING${NC}"
echo -e " ${YELLOW}Start with: sudo systemctl start thesis-work-tracker@\$(whoami).service${NC}"
fi
echo ""
# Useful commands
echo -e "${BOLD}Useful Commands:${NC}"
echo " • View live logs: tail -f /var/log/thesis-work-tracker/tracker.log"
echo " • Service status: systemctl status thesis-work-tracker@\$(whoami).service"
echo " • Restart tracker: sudo systemctl restart thesis-work-tracker@\$(whoami).service"
echo ""

View File

@ -1,565 +0,0 @@
#!/bin/bash
# Bachelor Thesis Work Tracker
# Monitors active windows for thesis-related work (Unreal Engine, Unity, Nvidia Omniverse, VS Code with specific repo)
# Unlocks Steam and other distractions only after sufficient work time is accumulated
#
# This daemon runs continuously and:
# 1. Tracks active window time for approved thesis work applications
# 2. Maintains a protected state file with accumulated work time
# 3. Manages hosts file blocking/unblocking based on work quota
# 4. Provides psychological friction against circumvention
set -euo pipefail
# Configuration
# shellcheck disable=SC2034 # SCRIPT_DIR reserved for future use
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
STATE_DIR="/var/lib/thesis-work-tracker"
STATE_FILE="$STATE_DIR/work-time.state"
LOCK_FILE="$STATE_DIR/tracker.lock"
LOG_DIR="/var/log/thesis-work-tracker"
LOG_FILE="$LOG_DIR/tracker.log"
CHECK_INTERVAL=15 # Check every 15 seconds
PROC_ROOT="${PROC_ROOT:-/proc}"
HOSTS_FILE="${HOSTS_FILE:-/etc/hosts}"
# Work requirements (in seconds)
# 2 hours of work = 7200 seconds required before Steam access
WORK_QUOTA_REQUIRED=7200 # 2 hours
WORK_DECAY_PER_HOUR=1800 # Lose 30 minutes per hour of Steam usage
# Thesis work applications - process names and window patterns
# These are the applications that count as "thesis work"
declare -A THESIS_APPS=(
["UnrealEditor"]="Unreal Engine"
["UE4Editor"]="Unreal Engine 4"
["UE5Editor"]="Unreal Engine 5"
["Unity"]="Unity Editor"
["UnityHub"]="Unity Hub"
["Code"]="Visual Studio Code" # Special handling for repo check
["code"]="Visual Studio Code" # lowercase variant
["omniverse"]="Nvidia Omniverse"
["kit"]="Nvidia Omniverse Kit"
)
# VS Code specific repo to track
VSCODE_REQUIRED_REPO="praca_magisterska"
# Steam and distraction patterns for hosts blocking
STEAM_DOMAINS=(
"steampowered.com"
"steamcommunity.com"
"steamgames.com"
"store.steampowered.com"
"steamcdn-a.akamaihd.net"
"steamstatic.com"
"steamusercontent.com"
)
# Additional distraction sites that should be blocked
DISTRACTION_DOMAINS=(
"reddit.com"
"twitter.com"
"x.com"
"facebook.com"
"instagram.com"
"youtube.com"
"twitch.tv"
"9gag.com"
"imgur.com"
)
# Colors for logging
# shellcheck disable=SC2034 # Colors available for log formatting
RED='\033[0;31m'
# shellcheck disable=SC2034
GREEN='\033[0;32m'
# shellcheck disable=SC2034
YELLOW='\033[0;33m'
# shellcheck disable=SC2034
BLUE='\033[0;34m'
# shellcheck disable=SC2034
CYAN='\033[0;36m'
# shellcheck disable=SC2034
BOLD='\033[1m'
# shellcheck disable=SC2034
NC='\033[0m' # No Color
# Logging function
log_message() {
local level="$1"
shift
local message="$*"
local timestamp
printf -v timestamp '%(%Y-%m-%d %H:%M:%S)T' -1
local formatted
formatted="[${timestamp}] [${level}] ${message}"
printf '%s\n' "$formatted" >&2
printf '%s\n' "$formatted" >> "$LOG_FILE" 2>/dev/null || true
}
log_info() { log_message "INFO" "$@"; }
log_warn() { log_message "WARN" "$@"; }
log_error() { log_message "ERROR" "$@"; }
log_debug() {
if [[ ${DEBUG:-0} -eq 1 ]]; then
log_message "DEBUG" "$@"
fi
}
wait_seconds() {
local timeout_s=$1
local start_ts end_ts elapsed_s remaining_s
printf -v start_ts '%(%s)T' -1
IFS= read -r -t "$timeout_s" || true
printf -v end_ts '%(%s)T' -1
elapsed_s=$((end_ts - start_ts))
if (( elapsed_s < timeout_s )); then
remaining_s=$((timeout_s - elapsed_s))
sleep "$remaining_s"
fi
}
# Initialize directories and state file
init_state() {
# Create directories with proper permissions
if [[ ! -d $STATE_DIR ]]; then
sudo mkdir -p "$STATE_DIR"
sudo chmod 700 "$STATE_DIR"
fi
if [[ ! -d $LOG_DIR ]]; then
sudo mkdir -p "$LOG_DIR"
sudo chmod 755 "$LOG_DIR"
fi
# Initialize state file if it doesn't exist
if [[ ! -f $STATE_FILE ]]; then
local now_iso now_epoch
printf -v now_iso '%(%Y-%m-%d %H:%M:%S)T' -1
printf -v now_epoch '%(%s)T' -1
sudo bash -c "cat > '$STATE_FILE'" <<EOF
# Thesis Work Tracker State File
# DO NOT EDIT MANUALLY - Managed by thesis_work_tracker daemon
# Last updated: ${now_iso}
TOTAL_WORK_SECONDS=0
LAST_UPDATE_TIMESTAMP=${now_epoch}
STEAM_ACCESS_GRANTED=0
LAST_WORK_SESSION_START=0
CURRENT_SESSION_SECONDS=0
EOF
sudo chmod 600 "$STATE_FILE"
if ! sudo chattr +i "$STATE_FILE" 2>/dev/null; then
log_warn "Failed to set immutable flag on state file - protections may be weaker"
fi
fi
}
# Load current state from file
load_state() {
if [[ ! -f $STATE_FILE ]]; then
log_error "State file not found: $STATE_FILE"
return 1
fi
# Temporarily remove immutable flag to read
sudo chattr -i "$STATE_FILE" 2>/dev/null || true
# Parse state file safely without using source or external text helpers.
TOTAL_WORK_SECONDS=0
STEAM_ACCESS_GRANTED=0
CURRENT_SESSION_SECONDS=0
LAST_WORK_SESSION_START=0
local key value
while IFS='=' read -r key value; do
case $key in
TOTAL_WORK_SECONDS)
TOTAL_WORK_SECONDS=$value
;;
STEAM_ACCESS_GRANTED)
STEAM_ACCESS_GRANTED=$value
;;
CURRENT_SESSION_SECONDS)
CURRENT_SESSION_SECONDS=$value
;;
LAST_WORK_SESSION_START)
LAST_WORK_SESSION_START=$value
;;
esac
done < "$STATE_FILE"
# Validate that values are numeric
if ! [[ $TOTAL_WORK_SECONDS =~ ^[0-9]+$ ]]; then TOTAL_WORK_SECONDS=0; fi
if ! [[ $STEAM_ACCESS_GRANTED =~ ^[01]$ ]]; then STEAM_ACCESS_GRANTED=0; fi
if ! [[ $CURRENT_SESSION_SECONDS =~ ^[0-9]+$ ]]; then CURRENT_SESSION_SECONDS=0; fi
if ! [[ $LAST_WORK_SESSION_START =~ ^[0-9]+$ ]]; then LAST_WORK_SESSION_START=0; fi
# Re-apply immutable flag
sudo chattr +i "$STATE_FILE" 2>/dev/null || true
}
# Save current state to file
save_state() {
local total_work="$1"
local steam_access="$2"
local current_session="$3"
local session_start="$4"
# Remove immutable flag
sudo chattr -i "$STATE_FILE" 2>/dev/null || true
local now_iso now_epoch
printf -v now_iso '%(%Y-%m-%d %H:%M:%S)T' -1
printf -v now_epoch '%(%s)T' -1
if [[ -w $STATE_FILE ]]; then
cat <<EOF > "$STATE_FILE"
# Thesis Work Tracker State File
# DO NOT EDIT MANUALLY - Managed by thesis_work_tracker daemon
# Last updated: ${now_iso}
TOTAL_WORK_SECONDS=$total_work
LAST_UPDATE_TIMESTAMP=${now_epoch}
STEAM_ACCESS_GRANTED=$steam_access
LAST_WORK_SESSION_START=$session_start
CURRENT_SESSION_SECONDS=$current_session
EOF
else
# Non-writable path: write in temp and copy with sudo.
local temp_state
temp_state=$(mktemp) || {
log_error "Failed to create temporary state file"
return 1
}
cat <<EOF > "$temp_state"
# Thesis Work Tracker State File
# DO NOT EDIT MANUALLY - Managed by thesis_work_tracker daemon
# Last updated: ${now_iso}
TOTAL_WORK_SECONDS=$total_work
LAST_UPDATE_TIMESTAMP=${now_epoch}
STEAM_ACCESS_GRANTED=$steam_access
LAST_WORK_SESSION_START=$session_start
CURRENT_SESSION_SECONDS=$current_session
EOF
sudo cp "$temp_state" "$STATE_FILE"
rm -f "$temp_state"
fi
sudo chmod 600 "$STATE_FILE"
# Re-apply immutable flag
if ! sudo chattr +i "$STATE_FILE" 2>/dev/null; then
log_warn "Failed to set immutable flag on state file after save"
fi
}
# Get active window title and process name
get_active_window_info() {
if ! command -v xdotool &>/dev/null; then
log_error "xdotool not installed, cannot detect active window"
return 1
fi
local active_window_id
active_window_id=$(xdotool getactivewindow 2>/dev/null || echo "")
if [[ -z $active_window_id ]]; then
return 1
fi
local window_name
local window_pid
window_pid=$(xdotool getwindowpid "$active_window_id" 2>/dev/null || echo "")
local process_name=""
if [[ -n $window_pid ]]; then
if [[ -r "$PROC_ROOT/$window_pid/comm" ]]; then
read -r process_name < "$PROC_ROOT/$window_pid/comm" || process_name=""
else
process_name=$(ps -p "$window_pid" -o comm= 2>/dev/null || echo "")
fi
fi
window_name=""
if [[ $process_name == "Code" || $process_name == "code" ]]; then
window_name=$(xdotool getwindowname "$active_window_id" 2>/dev/null || echo "")
fi
echo "${process_name}|${window_name}"
}
# Check if VS Code is working on the required repository
is_vscode_on_thesis_repo() {
local window_title="$1"
# VS Code window titles typically contain the folder/workspace name
# Look for the repo name in the window title
# Window title format is usually: "filename - reponame - Visual Studio Code"
if [[ $window_title == *"$VSCODE_REQUIRED_REPO"* ]]; then
return 0
fi
return 1
}
# Check if current active window is thesis work
is_thesis_work_active() {
local window_info
window_info=$(get_active_window_info)
if [[ -z $window_info ]]; then
return 1
fi
local process_name
local window_title
IFS='|' read -r process_name window_title <<<"$window_info"
log_debug "Active window: process='$process_name' title='$window_title'"
# Check each thesis application
for proc_pattern in "${!THESIS_APPS[@]}"; do
local app_name="${THESIS_APPS[$proc_pattern]}"
# Check window title for application name (more reliable than process name)
if [[ $window_title == *"$app_name"* ]]; then
# Special handling for VS Code - must be on thesis repo
if [[ $proc_pattern == "Code" ]] || [[ $proc_pattern == "code" ]]; then
if is_vscode_on_thesis_repo "$window_title"; then
log_debug "Thesis work detected: VS Code on $VSCODE_REQUIRED_REPO"
return 0
else
log_debug "VS Code detected but not on thesis repo"
continue
fi
fi
log_debug "Thesis work detected: $app_name"
return 0
fi
# Also check process name with exact match
if [[ $process_name == "$proc_pattern" ]]; then
# Special handling for VS Code - must be on thesis repo
if [[ $proc_pattern == "Code" ]] || [[ $proc_pattern == "code" ]]; then
if is_vscode_on_thesis_repo "$window_title"; then
log_debug "Thesis work detected: VS Code on $VSCODE_REQUIRED_REPO"
return 0
else
log_debug "VS Code detected but not on thesis repo"
continue
fi
fi
log_debug "Thesis work detected: $app_name"
return 0
fi
done
return 1
}
# Block Steam and distractions in /etc/hosts
block_distractions() {
log_info "Blocking Steam and distractions in /etc/hosts"
# Remove immutable flag temporarily
sudo chattr -i "$HOSTS_FILE" 2>/dev/null || true
# Scan the file once to build a set of already-blocked domains (no grep fork)
local -A blocked_set=()
local scan_line scan_domain
while IFS= read -r scan_line; do
if [[ $scan_line == "0.0.0.0 "* || $scan_line == $'0.0.0.0\t'* ]]; then
read -r _ scan_domain <<<"$scan_line" 2>/dev/null || true
blocked_set[$scan_domain]=1
fi
done < "$HOSTS_FILE"
# Collect entries not yet present
local new_entries=() domain
for domain in "${STEAM_DOMAINS[@]}" "${DISTRACTION_DOMAINS[@]}"; do
if [[ -z ${blocked_set[$domain]+x} ]]; then
new_entries+=("0.0.0.0 $domain")
fi
done
if (( ${#new_entries[@]} > 0 )); then
printf '%s\n' "${new_entries[@]}" | sudo bash -c "cat >> '$HOSTS_FILE'"
log_info "Added distraction blocks to /etc/hosts"
fi
# Re-apply immutable flag
sudo chattr +i "$HOSTS_FILE" 2>/dev/null || true
}
# Unblock Steam and distractions from /etc/hosts
unblock_distractions() {
log_info "Unblocking Steam and distractions in /etc/hosts"
# Remove immutable flag temporarily
sudo chattr -i "$HOSTS_FILE" 2>/dev/null || true
# Filter out blocked entries using bash (no sed/mktemp forks)
local new_content="" hosts_line skip domain
while IFS= read -r hosts_line; do
skip=0
for domain in "${STEAM_DOMAINS[@]}" "${DISTRACTION_DOMAINS[@]}"; do
if [[ $hosts_line == "0.0.0.0 $domain" || $hosts_line == "0.0.0.0 $domain" ]]; then
skip=1
break
fi
done
if [[ $skip -eq 0 ]]; then
new_content+="${hosts_line}"$'\n'
fi
done < "$HOSTS_FILE"
printf '%s' "$new_content" | sudo bash -c "cat > '$HOSTS_FILE'"
sudo chmod 644 "$HOSTS_FILE"
# Re-apply immutable flag
sudo chattr +i "$HOSTS_FILE" 2>/dev/null || true
log_info "Removed distraction blocks from /etc/hosts"
}
# Check if Steam is currently running (to track decay)
is_steam_running() {
local comm_file proc_name
for comm_file in "$PROC_ROOT"/[0-9]*/comm; do
[[ -r $comm_file ]] || continue
read -r proc_name < "$comm_file" || continue
if [[ $proc_name == "steam" ]]; then
return 0
fi
done
return 1
}
# Main tracking loop
main_loop() {
log_info "Starting thesis work tracker daemon"
# Initialize state
init_state
# Load initial state
load_state
local total_work_seconds=${TOTAL_WORK_SECONDS:-0}
local steam_access=${STEAM_ACCESS_GRANTED:-0}
local session_start=${LAST_WORK_SESSION_START:-0}
local session_seconds=${CURRENT_SESSION_SECONDS:-0}
# Apply initial blocking state
if [[ $steam_access -eq 0 ]]; then
block_distractions
fi
local last_status_log
printf -v last_status_log '%(%s)T' -1
local last_decay_check
printf -v last_decay_check '%(%s)T' -1
while true; do
local current_time
printf -v current_time '%(%s)T' -1
# Check if thesis work is active
if is_thesis_work_active; then
# Track work time
if [[ $session_start -eq 0 ]]; then
session_start=$current_time
log_info "Thesis work session started"
fi
# Increment session time
session_seconds=$((session_seconds + CHECK_INTERVAL))
total_work_seconds=$((total_work_seconds + CHECK_INTERVAL))
# Check if we've reached the quota
if [[ $total_work_seconds -ge $WORK_QUOTA_REQUIRED ]] && [[ $steam_access -eq 0 ]]; then
log_info "Work quota reached! Granting Steam access."
steam_access=1
unblock_distractions
fi
else
# No thesis work active
if [[ $session_start -ne 0 ]]; then
log_info "Thesis work session ended. Session duration: $((session_seconds / 60)) minutes"
session_start=0
session_seconds=0
fi
# Check for Steam usage and apply decay
if [[ $steam_access -eq 1 ]] && is_steam_running; then
local time_since_decay=$((current_time - last_decay_check))
if [[ $time_since_decay -ge 3600 ]]; then # Every hour
total_work_seconds=$((total_work_seconds - WORK_DECAY_PER_HOUR))
if [[ $total_work_seconds -lt 0 ]]; then
total_work_seconds=0
fi
last_decay_check=$current_time
log_info "Steam usage detected. Applied decay. Remaining work time: $((total_work_seconds / 60)) minutes"
# Revoke access if below quota
if [[ $total_work_seconds -lt $WORK_QUOTA_REQUIRED ]]; then
log_info "Work quota depleted. Revoking Steam access."
steam_access=0
block_distractions
fi
fi
fi
fi
# Save state periodically
save_state "$total_work_seconds" "$steam_access" "$session_seconds" "$session_start"
# Log status every 5 minutes
if [[ $((current_time - last_status_log)) -ge 300 ]]; then
local work_minutes=$((total_work_seconds / 60))
local quota_minutes=$((WORK_QUOTA_REQUIRED / 60))
local remaining_minutes=$((quota_minutes - work_minutes))
if [[ $remaining_minutes -lt 0 ]]; then
remaining_minutes=0
fi
log_info "Status: Total work time: ${work_minutes}m / ${quota_minutes}m | Steam access: $steam_access | Need: ${remaining_minutes}m more"
last_status_log=$current_time
fi
wait_seconds "$CHECK_INTERVAL"
done
}
# Handle signals for graceful shutdown
cleanup() {
log_info "Received shutdown signal, saving state and exiting"
rm -f "$LOCK_FILE"
exit 0
}
if [[ ${THESIS_WORK_TRACKER_SKIP_MAIN:-0} -ne 1 ]]; then
trap cleanup SIGTERM SIGINT
# Check for lock file to prevent multiple instances
if [[ -f $LOCK_FILE ]]; then
log_error "Another instance is already running (lock file exists: $LOCK_FILE)"
exit 1
fi
# Create lock file
touch "$LOCK_FILE"
# Run main loop
main_loop
fi

View File

@ -296,7 +296,6 @@ scripts/periodic_background/digital_wellbeing/pacman/install_pacman_wrapper.sh
scripts/fixes/nvidia_troubleshoot.sh
sudo scripts/features/setup_activitywatch.sh
sudo scripts/utils/setup_media_organizer.sh
sudo scripts/periodic_background/digital_wellbeing/setup_pc_startup_monitor.sh
yes | sudo scripts/periodic_background/setup_periodic_system.sh
sudo scripts/single_use/setup_thorium_startup.sh
yes | protonup

View File

@ -0,0 +1,159 @@
#!/bin/bash
# Unified installer for all personal Linux system modules.
#
# CORE modules (always installed):
# 1. Workout screen locker python_pkg/screen_locker/
# 2. Hosts blocking setup linux_configuration/scripts/periodic_background/hosts/
# 3. Midnight shutdown timer setup_midnight_shutdown.sh
#
# SECONDARY modules (prompted unless --all / --none given):
# 4. Steam backlog enforcer python_pkg/steam_backlog_enforcer/
# 5. Pacman wrapper periodic_background/digital_wellbeing/pacman/
# 6. i3 configuration periodic_background/i3-configuration/
# 7. Compulsive opening block block_compulsive_opening.sh
# 8. Focus-mode daemon install_focus_mode_daemon.sh
#
# Usage:
# ./install_core_system.sh [--all | --none]
#
# Flags:
# --all Install all secondary modules without prompting
# --none Skip all secondary modules
# -h Show this help
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LINUX_CONFIG="$(cd "$SCRIPT_DIR/../.." && pwd)"
REPO_ROOT="$(cd "$LINUX_CONFIG/.." && pwd)"
# ── Colour helpers ───────────────────────────────────────────────────────────
bold() { printf '\e[1m%s\e[0m' "$*"; }
green() { printf '\e[1;32m%s\e[0m' "$*"; }
yellow() { printf '\e[1;33m%s\e[0m' "$*"; }
red() { printf '\e[1;31m%s\e[0m' "$*"; }
header() { printf '\n%s\n%s\n' "$(bold "=== $1 ===")" "$(printf '=%.0s' {1..50})"; }
ok() { printf '%s %s\n' "$(green "✓")" "$*"; }
skip() { printf '%s %s\n' "$(yellow "")" "$*"; }
fail() { printf '%s %s\n' "$(red "✗")" "$*"; }
# ── Argument parsing ─────────────────────────────────────────────────────────
SECONDARY_MODE="ask" # ask | all | none
for arg in "$@"; do
case "$arg" in
--all) SECONDARY_MODE="all" ;;
--none) SECONDARY_MODE="none" ;;
-h | --help)
sed -n '2,/^$/p' "$0"
exit 0
;;
*)
printf 'Unknown option: %s\n' "$arg" >&2
exit 1
;;
esac
done
# ── Result tracking ──────────────────────────────────────────────────────────
declare -a INSTALLED=()
declare -a SKIPPED=()
declare -a FAILED=()
run_installer() {
local name="$1"
shift
header "$name"
if "$@"; then
ok "$name installed"
INSTALLED+=("$name")
else
fail "$name failed (exit $?)"
FAILED+=("$name")
fi
}
ask_install() {
# ask_install <name> <command...>
# Prompts user; respects SECONDARY_MODE override.
local name="$1"
shift
if [[ $SECONDARY_MODE == "none" ]]; then
skip "$name (--none)"
SKIPPED+=("$name")
return
fi
if [[ $SECONDARY_MODE == "all" ]]; then
run_installer "$name" "$@"
return
fi
# interactive
local answer
printf '\nInstall %s? [y/N] ' "$(bold "$name")"
read -r answer
if [[ "${answer,,}" == "y" ]]; then
run_installer "$name" "$@"
else
skip "$name"
SKIPPED+=("$name")
fi
}
# ── Summary ──────────────────────────────────────────────────────────────────
print_summary() {
printf '\n%s\n' "$(bold "========== INSTALL SUMMARY ==========")"
if [[ ${#INSTALLED[@]} -gt 0 ]]; then
printf '%s\n' "$(green "Installed (${#INSTALLED[@]}):")"
for m in "${INSTALLED[@]}"; do printf ' %s %s\n' "$(green "✓")" "$m"; done
fi
if [[ ${#SKIPPED[@]} -gt 0 ]]; then
printf '%s\n' "$(yellow "Skipped (${#SKIPPED[@]}):")"
for m in "${SKIPPED[@]}"; do printf ' %s %s\n' "$(yellow "")" "$m"; done
fi
if [[ ${#FAILED[@]} -gt 0 ]]; then
printf '%s\n' "$(red "Failed (${#FAILED[@]}):")"
for m in "${FAILED[@]}"; do printf ' %s %s\n' "$(red "✗")" "$m"; done
return 1
fi
}
# ═══════════════════════════════════════════════════════════════════════════════
# CORE MODULES (always installed)
# ═══════════════════════════════════════════════════════════════════════════════
printf '\n%s\n' "$(bold "Installing CORE modules…")"
run_installer "Workout screen locker" \
bash "$REPO_ROOT/python_pkg/screen_locker/install_systemd.sh"
run_installer "Hosts blocking" \
bash "$LINUX_CONFIG/scripts/periodic_background/hosts/install.sh"
run_installer "Midnight shutdown timer" \
bash "$LINUX_CONFIG/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh"
# ═══════════════════════════════════════════════════════════════════════════════
# SECONDARY MODULES (prompted unless --all / --none)
# ═══════════════════════════════════════════════════════════════════════════════
printf '\n%s\n' "$(bold "Secondary modules (${SECONDARY_MODE})…")"
ask_install "Steam backlog enforcer" \
bash "$REPO_ROOT/python_pkg/steam_backlog_enforcer/install.sh"
ask_install "Pacman wrapper" \
bash "$LINUX_CONFIG/scripts/periodic_background/digital_wellbeing/pacman/install_pacman_wrapper.sh"
ask_install "i3 configuration" \
bash "$LINUX_CONFIG/scripts/periodic_background/i3-configuration/install.sh"
ask_install "Compulsive opening blockade" \
sudo bash "$LINUX_CONFIG/scripts/periodic_background/digital_wellbeing/block_compulsive_opening.sh" install
ask_install "Focus-mode daemon" \
bash "$LINUX_CONFIG/scripts/periodic_background/digital_wellbeing/install_focus_mode_daemon.sh" install
# ═══════════════════════════════════════════════════════════════════════════════
print_summary

View File

@ -1,88 +0,0 @@
#!/bin/bash
# Regression tests for thesis_work_status.sh state-parsing helper behavior.
set -euo pipefail
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
REPO_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd)
TARGET_SCRIPT="$REPO_DIR/scripts/periodic_background/digital_wellbeing/thesis_work_status.sh"
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
assert_equals() {
local expected="$1"
local actual="$2"
local context="$3"
if [[ "$expected" != "$actual" ]]; then
fail "$context (expected: '$expected', actual: '$actual')"
fi
}
TMP_DIR=$(mktemp -d)
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
WORKTREE="$TMP_DIR/worktree"
BIN_DIR="$TMP_DIR/bin"
mkdir -p "$WORKTREE/scripts/periodic_background/digital_wellbeing" "$BIN_DIR"
cp "$TARGET_SCRIPT" "$WORKTREE/scripts/periodic_background/digital_wellbeing/thesis_work_status.sh"
# sudo stub — passes through all commands
cat >"$BIN_DIR/sudo" <<'EOF'
#!/bin/bash
"$@"
EOF
chmod +x "$BIN_DIR/sudo"
# chattr stub
cat >"$BIN_DIR/chattr" <<'EOF'
#!/bin/bash
exit 0
EOF
chmod +x "$BIN_DIR/chattr"
# grep stub — must NOT be called by state parsing
cat >"$BIN_DIR/grep" <<'EOF'
#!/bin/bash
printf 'grep should not be called\n' >&2
exit 1
EOF
chmod +x "$BIN_DIR/grep"
# cut stub — must NOT be called by state parsing
cat >"$BIN_DIR/cut" <<'EOF'
#!/bin/bash
printf 'cut should not be called\n' >&2
exit 1
EOF
chmod +x "$BIN_DIR/cut"
STATE_PATH="$TMP_DIR/work-time.state"
cat >"$STATE_PATH" <<'EOF'
# Thesis Work Tracker State File
TOTAL_WORK_SECONDS=3600
LAST_UPDATE_TIMESTAMP=1715400000
STEAM_ACCESS_GRANTED=1
LAST_WORK_SESSION_START=42
CURRENT_SESSION_SECONDS=90
EOF
printf 'Checking state parsing does not depend on grep/cut...\n'
# THESIS_STATUS_SKIP_SUDO=1 prevents exec sudo re-exec
# THESIS_STATUS_SKIP_OUTPUT=1 prevents display output; script returns after parsing
parsed_vals=$(PATH="$BIN_DIR:$PATH" THESIS_STATUS_SKIP_SUDO=1 THESIS_STATUS_SKIP_OUTPUT=1 \
bash -lc \
"STATE_FILE='$STATE_PATH'; \
source '$WORKTREE/scripts/periodic_background/digital_wellbeing/thesis_work_status.sh'; \
printf '%s|%s|%s|%s' \"\$TOTAL_WORK_SECONDS\" \"\$STEAM_ACCESS_GRANTED\" \"\$CURRENT_SESSION_SECONDS\" \"\$LAST_WORK_SESSION_START\"" \
2>/dev/null)
assert_equals '3600|1|90|42' "$parsed_vals" \
'thesis_work_status state parsing should work without grep/cut dependency'
printf 'thesis_work_status.sh regression checks passed.\n'

View File

@ -1,343 +0,0 @@
#!/bin/bash
# Regression tests for thesis_work_tracker.sh helper behavior.
set -euo pipefail
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
REPO_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd)
TARGET_SCRIPT="$REPO_DIR/scripts/periodic_background/digital_wellbeing/thesis_work_tracker.sh"
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
assert_equals() {
local expected="$1"
local actual="$2"
local context="$3"
if [[ "$expected" != "$actual" ]]; then
fail "$context (expected: '$expected', actual: '$actual')"
fi
}
TMP_DIR=$(mktemp -d)
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
WORKTREE="$TMP_DIR/worktree"
BIN_DIR="$TMP_DIR/bin"
mkdir -p "$WORKTREE/scripts/periodic_background/digital_wellbeing" "$BIN_DIR"
cp "$TARGET_SCRIPT" "$WORKTREE/scripts/periodic_background/digital_wellbeing/thesis_work_tracker.sh"
cat >"$BIN_DIR/xdotool" <<'EOF'
#!/bin/bash
case "$*" in
'getactivewindow')
printf '12345\n'
;;
'getwindowpid 12345')
printf '6789\n'
;;
'getwindowname 12345')
printf '%s\n' "${MOCK_WINDOW_TITLE:-Document - praca_magisterska - Visual Studio Code}"
;;
*)
printf 'unexpected xdotool args: %s\n' "$*" >&2
exit 1
;;
esac
EOF
chmod +x "$BIN_DIR/xdotool"
cat >"$BIN_DIR/ps" <<'EOF'
#!/bin/bash
printf '%s\n' "${MOCK_PROCESS_NAME:-Code}"
EOF
chmod +x "$BIN_DIR/ps"
cat >"$BIN_DIR/pgrep" <<'EOF'
#!/bin/bash
exit 1
EOF
chmod +x "$BIN_DIR/pgrep"
cat >"$BIN_DIR/date" <<'EOF'
#!/bin/bash
printf 'date should not be called\n' >&2
exit 1
EOF
chmod +x "$BIN_DIR/date"
source_env() {
PATH="$BIN_DIR:$PATH" THESIS_WORK_TRACKER_SKIP_MAIN=1 bash -lc "source '$WORKTREE/scripts/periodic_background/digital_wellbeing/thesis_work_tracker.sh'; $1"
}
source_env_with_proc() {
local proc_root="$1"
local cmd="$2"
PATH="$BIN_DIR:$PATH" PROC_ROOT="$proc_root" THESIS_WORK_TRACKER_SKIP_MAIN=1 bash -lc "source '$WORKTREE/scripts/periodic_background/digital_wellbeing/thesis_work_tracker.sh'; $cmd"
}
printf 'Checking helper output for VS Code on thesis repo...\n'
result=$(source_env 'get_active_window_info')
assert_equals 'Code|Document - praca_magisterska - Visual Studio Code' "$result" \
'get_active_window_info should return process and title for VS Code'
printf 'Checking helper reads process name from /proc before ps...\n'
PROC_WINDOW_DIR="$TMP_DIR/proc-window"
mkdir -p "$PROC_WINDOW_DIR/6789"
printf 'Code\n' >"$PROC_WINDOW_DIR/6789/comm"
cat >"$BIN_DIR/ps" <<'EOF'
#!/bin/bash
printf 'ps should not be called\n' >&2
exit 1
EOF
chmod +x "$BIN_DIR/ps"
result_proc=$(MOCK_WINDOW_TITLE='Document - praca_magisterska - Visual Studio Code' \
source_env_with_proc "$PROC_WINDOW_DIR" 'get_active_window_info')
assert_equals 'Code|Document - praca_magisterska - Visual Studio Code' "$result_proc" \
'get_active_window_info should read process name from proc comm without ps fallback'
cat >"$BIN_DIR/ps" <<'EOF'
#!/bin/bash
printf '%s\n' "${MOCK_PROCESS_NAME:-Code}"
EOF
chmod +x "$BIN_DIR/ps"
printf 'Checking thesis detection for VS Code thesis repo...\n'
active=$(source_env 'is_thesis_work_active && printf yes || printf no')
assert_equals 'yes' "$active" 'thesis detection should accept VS Code on the thesis repo'
printf 'Checking thesis detection skips non-thesis VS Code windows...\n'
non_thesis=$(MOCK_WINDOW_TITLE='Document - notes - Visual Studio Code' source_env 'is_thesis_work_active && printf yes || printf no')
assert_equals 'no' "$non_thesis" 'thesis detection should reject VS Code outside the thesis repo'
printf 'Checking steam detection reads process state from /proc without pgrep...\n'
PROC_DIR="$TMP_DIR/proc"
mkdir -p "$PROC_DIR/999"
printf 'steam\n' >"$PROC_DIR/999/comm"
steam_running=$(source_env_with_proc "$PROC_DIR" 'is_steam_running && printf yes || printf no')
assert_equals 'yes' "$steam_running" 'is_steam_running should detect steam via proc comm files'
printf 'Checking logging path does not depend on tee...\n'
cat >"$BIN_DIR/tee" <<'EOF'
#!/bin/bash
printf 'tee should not be called\n' >&2
exit 1
EOF
chmod +x "$BIN_DIR/tee"
LOG_PATH="$TMP_DIR/tracker.log"
set +e
log_result=$(PATH="$BIN_DIR:$PATH" THESIS_WORK_TRACKER_SKIP_MAIN=1 bash -lc \
"source '$WORKTREE/scripts/periodic_background/digital_wellbeing/thesis_work_tracker.sh'; LOG_FILE='$LOG_PATH'; log_info 'logging regression test'; printf ok")
log_ec=$?
set -e
assert_equals '0' "$log_ec" 'log_info should not fail when tee is unavailable'
assert_equals 'ok' "$log_result" 'log_info should succeed without tee dependency'
grep -q 'logging regression test' "$LOG_PATH" \
|| fail 'log_info should append message to the log file'
printf 'Checking state loading does not depend on grep/cut...\n'
cat >"$BIN_DIR/sudo" <<'EOF'
#!/bin/bash
"$@"
EOF
chmod +x "$BIN_DIR/sudo"
cat >"$BIN_DIR/chattr" <<'EOF'
#!/bin/bash
exit 0
EOF
chmod +x "$BIN_DIR/chattr"
cat >"$BIN_DIR/grep" <<'EOF'
#!/bin/bash
printf 'grep should not be called\n' >&2
exit 1
EOF
chmod +x "$BIN_DIR/grep"
cat >"$BIN_DIR/cut" <<'EOF'
#!/bin/bash
printf 'cut should not be called\n' >&2
exit 1
EOF
chmod +x "$BIN_DIR/cut"
STATE_PATH="$TMP_DIR/work-time.state"
cat >"$STATE_PATH" <<'EOF'
# Thesis Work Tracker State File
TOTAL_WORK_SECONDS=123
LAST_UPDATE_TIMESTAMP=1715400000
STEAM_ACCESS_GRANTED=1
LAST_WORK_SESSION_START=77
CURRENT_SESSION_SECONDS=15
EOF
loaded_state=$(PATH="$BIN_DIR:$PATH" THESIS_WORK_TRACKER_SKIP_MAIN=1 bash -lc \
"source '$WORKTREE/scripts/periodic_background/digital_wellbeing/thesis_work_tracker.sh'; \
STATE_FILE='$STATE_PATH'; \
load_state; \
printf '%s|%s|%s|%s' \"\$TOTAL_WORK_SECONDS\" \"\$STEAM_ACCESS_GRANTED\" \"\$CURRENT_SESSION_SECONDS\" \"\$LAST_WORK_SESSION_START\"")
assert_equals '123|1|15|77' "$loaded_state" 'load_state should parse values without grep/cut dependency'
printf 'Checking state saving does not depend on tee...\n'
set +e
save_state_result=$(PATH="$BIN_DIR:$PATH" THESIS_WORK_TRACKER_SKIP_MAIN=1 bash -lc \
"source '$WORKTREE/scripts/periodic_background/digital_wellbeing/thesis_work_tracker.sh'; \
STATE_FILE='$STATE_PATH'; \
STATE_DIR='$(dirname "$STATE_PATH")'; \
save_state 321 0 45 9; \
printf ok")
save_state_ec=$?
set -e
assert_equals '0' "$save_state_ec" 'save_state should not fail when tee is unavailable'
assert_equals 'ok' "$save_state_result" 'save_state should complete successfully without tee dependency'
saved_state=$(PATH="$BIN_DIR:$PATH" THESIS_WORK_TRACKER_SKIP_MAIN=1 bash -lc \
"source '$WORKTREE/scripts/periodic_background/digital_wellbeing/thesis_work_tracker.sh'; \
STATE_FILE='$STATE_PATH'; \
load_state; \
printf '%s|%s|%s|%s' \"\$TOTAL_WORK_SECONDS\" \"\$STEAM_ACCESS_GRANTED\" \"\$CURRENT_SESSION_SECONDS\" \"\$LAST_WORK_SESSION_START\"")
assert_equals '321|0|45|9' "$saved_state" 'save_state should persist updated values without tee dependency'
printf 'Checking writable save path does not require mktemp...\n'
cat >"$BIN_DIR/mktemp" <<'EOF'
#!/bin/bash
printf 'mktemp should not be called\n' >&2
exit 1
EOF
chmod +x "$BIN_DIR/mktemp"
set +e
save_fast_result=$(PATH="$BIN_DIR:$PATH" THESIS_WORK_TRACKER_SKIP_MAIN=1 bash -lc \
"source '$WORKTREE/scripts/periodic_background/digital_wellbeing/thesis_work_tracker.sh'; \
STATE_FILE='$STATE_PATH'; \
STATE_DIR='$(dirname "$STATE_PATH")'; \
save_state 654 1 30 11; \
printf ok")
save_fast_ec=$?
set -e
assert_equals '0' "$save_fast_ec" 'save_state should not require mktemp when state file is writable'
assert_equals 'ok' "$save_fast_result" 'save_state fast path should complete successfully'
saved_fast_state=$(PATH="$BIN_DIR:$PATH" THESIS_WORK_TRACKER_SKIP_MAIN=1 bash -lc \
"source '$WORKTREE/scripts/periodic_background/digital_wellbeing/thesis_work_tracker.sh'; \
STATE_FILE='$STATE_PATH'; \
load_state; \
printf '%s|%s|%s|%s' \"\$TOTAL_WORK_SECONDS\" \"\$STEAM_ACCESS_GRANTED\" \"\$CURRENT_SESSION_SECONDS\" \"\$LAST_WORK_SESSION_START\"")
assert_equals '654|1|30|11' "$saved_fast_state" 'save_state writable fast path should persist values'
printf 'Checking block_distractions does not depend on grep or tee...\n'
HOSTS_PATH="$TMP_DIR/hosts-block"
printf '# /etc/hosts baseline\n127.0.0.1 localhost\n' >"$HOSTS_PATH"
cat >"$BIN_DIR/grep" <<'EOF'
#!/bin/bash
printf 'grep should not be called\n' >&2
exit 1
EOF
chmod +x "$BIN_DIR/grep"
cat >"$BIN_DIR/tee" <<'EOF'
#!/bin/bash
printf 'tee should not be called\n' >&2
exit 1
EOF
chmod +x "$BIN_DIR/tee"
set +e
block_result=$(PATH="$BIN_DIR:$PATH" THESIS_WORK_TRACKER_SKIP_MAIN=1 bash -lc \
"source '$WORKTREE/scripts/periodic_background/digital_wellbeing/thesis_work_tracker.sh'; \
HOSTS_FILE='$HOSTS_PATH'; \
block_distractions; \
printf ok")
block_ec=$?
set -e
assert_equals '0' "$block_ec" 'block_distractions should succeed without grep/tee'
assert_equals 'ok' "$block_result" 'block_distractions should complete without grep/tee dependency'
grep -q '0.0.0.0 steampowered.com' "$HOSTS_PATH" \
|| fail 'block_distractions should add steampowered.com entry to hosts file'
grep -q '0.0.0.0 reddit.com' "$HOSTS_PATH" \
|| fail 'block_distractions should add reddit.com entry to hosts file'
grep -q 'localhost' "$HOSTS_PATH" \
|| fail 'block_distractions should preserve existing localhost entry'
printf 'Checking block_distractions is idempotent (no duplicate entries)...\n'
set +e
block_result2=$(PATH="$BIN_DIR:$PATH" THESIS_WORK_TRACKER_SKIP_MAIN=1 bash -lc \
"source '$WORKTREE/scripts/periodic_background/digital_wellbeing/thesis_work_tracker.sh'; \
HOSTS_FILE='$HOSTS_PATH'; \
block_distractions; \
printf ok")
block_ec2=$?
set -e
assert_equals '0' "$block_ec2" 'block_distractions second run should succeed'
assert_equals 'ok' "$block_result2" 'block_distractions idempotent run should complete successfully'
count=$(grep -c '0.0.0.0 steampowered.com' "$HOSTS_PATH" || true)
assert_equals '1' "$count" 'block_distractions should not add duplicate entries'
printf 'Checking unblock_distractions does not depend on sed or mktemp...\n'
HOSTS_UNBLOCK_PATH="$TMP_DIR/hosts-unblock"
printf '# /etc/hosts baseline\n127.0.0.1 localhost\n0.0.0.0 steampowered.com\n0.0.0.0 reddit.com\n0.0.0.0 youtube.com\n' >"$HOSTS_UNBLOCK_PATH"
cat >"$BIN_DIR/sed" <<'EOF'
#!/bin/bash
printf 'sed should not be called\n' >&2
exit 1
EOF
chmod +x "$BIN_DIR/sed"
cat >"$BIN_DIR/mktemp" <<'EOF'
#!/bin/bash
printf 'mktemp should not be called\n' >&2
exit 1
EOF
chmod +x "$BIN_DIR/mktemp"
set +e
unblock_result=$(PATH="$BIN_DIR:$PATH" THESIS_WORK_TRACKER_SKIP_MAIN=1 bash -lc \
"source '$WORKTREE/scripts/periodic_background/digital_wellbeing/thesis_work_tracker.sh'; \
HOSTS_FILE='$HOSTS_UNBLOCK_PATH'; \
unblock_distractions; \
printf ok")
unblock_ec=$?
set -e
assert_equals '0' "$unblock_ec" 'unblock_distractions should succeed without sed/mktemp'
assert_equals 'ok' "$unblock_result" 'unblock_distractions should complete without sed/mktemp dependency'
if grep -q '0.0.0.0 steampowered.com' "$HOSTS_UNBLOCK_PATH" 2>/dev/null; then
fail 'unblock_distractions should remove steampowered.com entry'
fi
if grep -q '0.0.0.0 reddit.com' "$HOSTS_UNBLOCK_PATH" 2>/dev/null; then
fail 'unblock_distractions should remove reddit.com entry'
fi
grep -q 'localhost' "$HOSTS_UNBLOCK_PATH" \
|| fail 'unblock_distractions should preserve localhost entry'
printf 'Checking init_state does not depend on tee...\n'
STATE_INIT_PATH="$TMP_DIR/init-state-file.state"
STATE_INIT_DIR="$TMP_DIR/init-state-dir"
mkdir -p "$STATE_INIT_DIR"
set +e
init_result=$(PATH="$BIN_DIR:$PATH" THESIS_WORK_TRACKER_SKIP_MAIN=1 bash -lc \
"source '$WORKTREE/scripts/periodic_background/digital_wellbeing/thesis_work_tracker.sh'; \
STATE_FILE='$STATE_INIT_PATH'; \
STATE_DIR='$STATE_INIT_DIR'; \
LOG_DIR='$TMP_DIR'; \
LOG_FILE='$TMP_DIR/init-tracker.log'; \
init_state; \
printf ok")
init_ec=$?
set -e
assert_equals '0' "$init_ec" 'init_state should succeed without tee dependency'
assert_equals 'ok' "$init_result" 'init_state should complete without tee dependency'
grep -q 'TOTAL_WORK_SECONDS=0' "$STATE_INIT_PATH" \
|| fail 'init_state should write TOTAL_WORK_SECONDS=0 to state file'
printf 'thesis_work_tracker.sh regression checks passed.\n'