diff --git a/docs/superpowers/contracts/music-parallelism-split-2026-08-16.json b/docs/superpowers/contracts/music-parallelism-split-2026-08-16.json new file mode 100644 index 00000000..6362eced --- /dev/null +++ b/docs/superpowers/contracts/music-parallelism-split-2026-08-16.json @@ -0,0 +1,20 @@ +{ + "title": "Split music_parallelism.sh (412 lines) into an entry script and three libs", + "objective": "Bring linux_configuration/scripts/periodic_background/digital_wellbeing/music_parallelism.sh under the repo-wide 250-line cap by moving its nine functions into lib/music_{detect,monitor,status}.sh, with the existing regression test proving behaviour is unchanged.", + "acceptance_criteria": [ + "music_parallelism.sh and each new lib/music_*.sh file is at most 250 lines.", + "shellcheck reports zero findings across the entry script and all three libs, at the repo's no-severity-filter setting, with no suppression added.", + "linux_configuration/tests/test_music_parallelism.sh exits 0 and runs the same five named checks as before the split.", + "The set of functions defined across the entry and the libs is identical to the nine defined at HEAD, each defined exactly once.", + "Every executable line of the original survives, except the three carrying the deliberate music_services -> active_services rename.", + "MUSIC_FOUND_PROCESS and MUSIC_FOUND_WINDOW, which find_music_services writes and kill_music_services reads, do not cross a file boundary.", + "No lib sources another lib; the entry sources all three, after the configuration arrays they read.", + "No assertion in the test is changed, deleted, skipped or weakened; the only permitted test edit is making the libs reachable from the temp worktree." + ], + "out_of_scope": [ + "Changing any detection pattern, interval, or the set of monitored music services or focus apps.", + "Converting the $0-based SCRIPT_DIR to ${BASH_SOURCE[0]}; nothing sources this script, so the existing form is correct here.", + "Touching the common.sh dual-path source or the /usr/local/lib fallback." + ], + "verifier": "bash linux_configuration/tests/test_music_parallelism.sh (expects exit 0 and the same five checks), shellcheck over the entry plus all three libs (expects zero findings), bash -n on each, bash ~/utils/scripts/check_file_length.sh over the four files (expects none listed), and a sorted line-set diff of the original against entry+libs showing only the deliberate rename." +} diff --git a/docs/superpowers/evidence/music-parallelism-split-2026-08-16.json b/docs/superpowers/evidence/music-parallelism-split-2026-08-16.json new file mode 100644 index 00000000..21d55a73 --- /dev/null +++ b/docs/superpowers/evidence/music-parallelism-split-2026-08-16.json @@ -0,0 +1,58 @@ +{ + "intent": "Bring music_parallelism.sh under the 250-line cap by moving its nine functions into three sourced libs, leaving its behaviour and its test outcomes unchanged.", + "scope": [ + "linux_configuration/scripts/periodic_background/digital_wellbeing/music_parallelism.sh (412 -> 135 lines)", + "New: lib/music_{detect,monitor,status}.sh", + "linux_configuration/tests/test_music_parallelism.sh -- harness only, so the libs reach the temp worktree", + "Non-goal: changing any assertion, interval, or detection pattern" + ], + "changes": [ + "Moved wait_seconds, contains_music_process, find_music_services and kill_music_services into lib/music_detect.sh. The first two of those four share the MUSIC_FOUND_PROCESS / MUSIC_FOUND_WINDOW pair, so they stay in one file and no mutable state crosses a boundary.", + "Moved notify_user, instant_monitor_loop and monitor_loop into lib/music_monitor.sh; notify_user is called only by those two loops.", + "Moved show_status and show_usage into lib/music_status.sh.", + "The entry script keeps the header, the common.sh dual-path source, all configuration arrays and the command dispatch case, and sources the three libs after the configuration they read.", + "Renamed monitor_loop's local music_services to active_services. shellcheck flagged the local as a possible misspelling of the MUSIC_SERVICES config array (SC2153) once both appeared in the same file; renaming the purely internal local is the fix, not a suppression.", + "Taught the test harness to copy the script's lib/ directory into its temp worktree alongside the script. No assertion, expectation or check was changed." + ], + "verification": [ + { + "command": "bash linux_configuration/tests/test_music_parallelism.sh", + "result": "pass", + "evidence": "Exit 0, the same five checks as before the split, by the same names: stable-focus backoff, conflict retry interval, idle interval, conflict-path xdotool deduplication, monitor-loop xdotool deduplication. Baseline was captured before any edit and matches." + }, + { + "command": "shellcheck music_parallelism.sh lib/music_*.sh", + "result": "pass", + "evidence": "Zero findings across all four files at the repo's no-severity-filter setting, with no suppression added." + }, + { + "command": "bash -n on the entry and each lib", + "result": "pass", + "evidence": "All four parse. This check earned its keep: the first splice dropped notify_user's closing brace, and shellcheck reported it only as SC1094 'parsing of sourced file failed' on the entry, which reads like a lint nicety rather than a broken file." + }, + { + "command": "bash ~/utils/scripts/check_file_length.sh", + "result": "pass", + "evidence": "entry 135, detect 133, monitor 91, status 85. None of the four appears in the violation list." + }, + { + "command": "diff of the function set against HEAD", + "result": "pass", + "evidence": "Identical: all nine functions preserved, each defined exactly once across the four files. No lib sources another; the entry sources all three." + }, + { + "command": "sorted line-set diff of HEAD:music_parallelism.sh against entry+libs concatenated", + "result": "pass", + "evidence": "The only differences are the three lines carrying the deliberate music_services -> active_services rename, and the three added source lines. No executable line lost." + } + ], + "risks": [ + "The test harness previously copied one file; it now copies a directory too. If a future split adds a lib elsewhere the same fix will be needed again, but the failure is loud -- the source line aborts the script and every check fails, which is how this one was caught.", + "The entry keeps the existing $0-based SCRIPT_DIR rather than ${BASH_SOURCE[0]}. Safe here because nothing sources music_parallelism.sh: it is invoked as a command by its dispatch case, and grep over linux_configuration finds no other reference than the test.", + "The script has a /usr/local/lib/common.sh fallback for its shared library, but no installed copy of the script itself exists (checked /usr/local/bin), so the new libs are only ever resolved from the repo." + ], + "rollback": [ + "git revert the commit; music_parallelism.sh returns to its 412-line single-file form, the three libs disappear, and the test harness stops copying a lib/ directory that no longer exists.", + "After rollback, re-run bash linux_configuration/tests/test_music_parallelism.sh and confirm the same five checks still pass." + ] +} diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/music_detect.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/music_detect.sh new file mode 100755 index 00000000..edbbe994 --- /dev/null +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/music_detect.sh @@ -0,0 +1,132 @@ +#!/bin/bash +# Music-service detection and termination, plus the shared wait helper. +# +# Sourced by music_parallelism.sh; split out to keep it under the 250-line cap. +# Sourced rather than run, so it inherits the caller's strict mode and reads +# the interval, MUSIC_* and PROC_ROOT configuration set above the source line. +# +# find_music_services and kill_music_services stay in one file: the +# MUSIC_FOUND_PROCESS / MUSIC_FOUND_WINDOW pair the first sets and the second +# reads would otherwise cross a file boundary. + +wait_seconds() { + local timeout_s=$1 + local start_ts end_ts elapsed_s remaining_s + + if [[ -n ${MUSIC_PARALLELISM_TEST_WAIT_LOG:-} ]]; then + printf '%s\n' "$timeout_s" >>"$MUSIC_PARALLELISM_TEST_WAIT_LOG" + if [[ ${MUSIC_PARALLELISM_TEST_EXIT_AFTER_WAIT:-0} -eq 1 ]]; then + exit 99 + fi + return 0 + fi + + 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 +} + +contains_music_process() { + local comm_file comm_lower token_lower + + for comm_file in "$PROC_ROOT"/[0-9]*/comm; do + [[ -r $comm_file ]] || continue + read -r comm_lower <"$comm_file" || continue + comm_lower=${comm_lower,,} + + for token_lower in "${MUSIC_PROCESS_NAMES[@]}"; do + if [[ $comm_lower == *"${token_lower,,}"* ]]; then + return 0 + fi + done + done + + return 1 +} + +# Check if any music service is running and return its details (OPTIMIZED: batch pgrep calls) +find_music_services() { + local found_services=() + MUSIC_FOUND_PROCESS=0 + MUSIC_FOUND_WINDOW=0 + + # Check processes using /proc (fork-free) + if contains_music_process; then + MUSIC_FOUND_PROCESS=1 + found_services+=("music process") + fi + + # Check windows (use optimized is_focus_app_running logic: single xdotool regex call) + if command -v xdotool &>/dev/null && [[ ${#MUSIC_SERVICES[@]} -gt 0 ]]; then + if xdotool search --name "$MUSIC_WINDOWS_PATTERN" &>/dev/null 2>&1; then + MUSIC_FOUND_WINDOW=1 + found_services+=("music service (window)") + fi + fi + + if [[ ${#found_services[@]} -gt 0 ]]; then + printf '%s\n' "${found_services[@]}" + return 0 + fi + return 1 +} + +# Kill music services +kill_music_services() { + local use_cached_detection="${1:-0}" + local killed=false + local window_pattern='YouTube Music|music\.youtube\.com|music\.apple\.com|soundcloud\.com|pandora\.com|deezer\.com|tidal\.com' + local should_check_windows=1 + local should_check_processes=1 + + if [[ $use_cached_detection -eq 1 ]]; then + should_check_windows=$MUSIC_FOUND_WINDOW + should_check_processes=$MUSIC_FOUND_PROCESS + fi + + # Close browser tabs for web-based music services via one xdotool search + if [[ $should_check_windows -eq 1 ]] && command -v xdotool &>/dev/null; then + local windows wid + windows=$(xdotool search --name "$window_pattern" 2>/dev/null || true) + for wid in $windows; do + [[ -n $wid ]] || continue + xdotool windowclose "$wid" 2>/dev/null || true + killed=true + done + fi + + # Kill app processes with /proc scan + builtin kill (fork-free in hot path) + if [[ $should_check_processes -eq 1 ]]; then + local comm_file pid comm_lower token_lower + for comm_file in "$PROC_ROOT"/[0-9]*/comm; do + [[ -r $comm_file ]] || continue + read -r comm_lower <"$comm_file" || continue + comm_lower=${comm_lower,,} + pid=${comm_file#"$PROC_ROOT"/} + pid=${pid%%/*} + + for token_lower in "${MUSIC_PROCESS_NAMES[@]}"; do + if [[ $comm_lower == *"${token_lower,,}"* ]]; then + if [[ $PROC_ROOT != "/proc" ]]; then + # Test mode (fake proc tree): mark as killed without signaling host PIDs. + killed=true + elif kill -9 "$pid" 2>/dev/null; then + killed=true + fi + break + fi + done + done + fi + + if $killed; then + return 0 + fi + return 1 +} diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/music_monitor.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/music_monitor.sh new file mode 100755 index 00000000..1ebe9845 --- /dev/null +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/music_monitor.sh @@ -0,0 +1,91 @@ +#!/bin/bash +# The two monitoring loops: the instant (adaptive-interval) loop and the +# fixed-interval fallback loop. +# +# Sourced by music_parallelism.sh; split out to keep it under the 250-line cap. +# Sourced rather than run, so it inherits the caller's strict mode and calls +# the detection helpers in music_detect.sh, which the entry sources first. + +# Send notification to user +notify_user() { + local focus_app="$1" + local message="Music stopped - focus mode active ($focus_app detected)" + + # Try to send desktop notification + if command -v notify-send &>/dev/null; then + notify-send -u normal -t 5000 "🎵 Music Parallelism" "$message" 2>/dev/null || true + fi + + log_message "$message" +} + +# Instant monitoring loop - uses polling at high frequency ONLY when focus app is detected +# When focus app active: checks every 0.5s. When idle: checks every 3s. Reduces fork overhead. +# OPTIMIZATION: Single batched pgrep call instead of multiple separate calls +instant_monitor_loop() { + local next_enforcement_ts=0 + local current_ts=0 + local focus_app="" + local sleep_interval="$IDLE_CHECK_INTERVAL" + + log_message "=== Music Parallelism INSTANT Monitor Started ===" + log_message "Focus apps (windows): ${FOCUS_APPS_WINDOWS[*]}" + log_message "Focus apps (processes): ${FOCUS_APPS_PROCESSES[*]}" + log_message "Polling: ${FAST_CHECK_INTERVAL}s active, ${ACTIVE_NO_MUSIC_INTERVAL}s stable-focus, ${IDLE_CHECK_INTERVAL}s idle, ${ENFORCEMENT_COOLDOWN}s enforcement cooldown" + + while true; do + if focus_app=$(is_focus_app_running 2>/dev/null); then + current_ts=$(get_timestamp) + if ((current_ts >= next_enforcement_ts)); then + if find_music_services >/dev/null 2>&1; then + if kill_music_services 1; then + notify_user "$focus_app" + log_message "INSTANT KILL: Music services terminated" + sleep_interval="$ACTIVE_AFTER_KILL_INTERVAL" + fi + else + sleep_interval="$ACTIVE_NO_MUSIC_INTERVAL" + fi + next_enforcement_ts=$((current_ts + ENFORCEMENT_COOLDOWN)) + else + sleep_interval="$ACTIVE_NO_MUSIC_INTERVAL" + fi + else + next_enforcement_ts=0 + sleep_interval="$IDLE_CHECK_INTERVAL" + fi + + wait_seconds "$sleep_interval" + done +} + +# Main monitoring loop +monitor_loop() { + log_message "=== Music Parallelism Monitor Started ===" + log_message "Focus apps (windows): ${FOCUS_APPS_WINDOWS[*]}" + log_message "Focus apps (processes): ${FOCUS_APPS_PROCESSES[*]}" + log_message "Music services monitored: ${MUSIC_SERVICES[*]}" + log_message "Check interval: ${CHECK_INTERVAL}s" + + while true; do + # Check if a focus app is running + local focus_app + if focus_app=$(is_focus_app_running); then + # Focus app detected, check for music services + # Named to avoid colliding with the MUSIC_SERVICES config array, + # which this file also reads. + local active_services + if active_services=$(find_music_services); then + log_message "Conflict detected: Focus app '$focus_app' running with music services" + log_message "Active music services: $active_services" + + # Kill the music services + if kill_music_services 1; then + notify_user "$focus_app" + fi + fi + fi + + wait_seconds "$CHECK_INTERVAL" + done +} diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/music_status.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/music_status.sh new file mode 100755 index 00000000..eeda35b5 --- /dev/null +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/lib/music_status.sh @@ -0,0 +1,85 @@ +#!/bin/bash +# The status report and the usage text. +# +# Sourced by music_parallelism.sh; split out to keep it under the 250-line cap. +# Sourced rather than run, so it inherits the caller's strict mode and reads +# the interval and MUSIC_* configuration set above the source line. + +# Show status +show_status() { + echo "Music Parallelism Monitor Status" + echo "=================================" + echo "" + + echo "Focus Applications (window-based detection):" + local focus_running=false + + # Check windows (OPTIMIZED: single xdotool call with combined regex) + if command -v xdotool &>/dev/null && [[ ${#FOCUS_APPS_WINDOWS[@]} -gt 0 ]]; then + local regex + printf -v regex '%s|' "${FOCUS_APPS_WINDOWS[@]}" + regex="${regex%|}" # strip trailing | + if xdotool search --name "$regex" &>/dev/null 2>&1; then + echo " ✓ Focus window detected" + focus_running=true + fi + fi + + # Check processes using shared /proc-based helper (fork-free) + if is_focus_app_running >/dev/null 2>&1; then + echo " ✓ Focus process running" + focus_running=true + fi + + if ! $focus_running; then + echo " (none detected)" + fi + + echo "" + echo "Music Services:" + local music_running=false + if music_services=$(find_music_services 2>/dev/null); then + echo "$music_services" | while read -r svc; do + echo " ♪ $svc (RUNNING)" + done + music_running=true + fi + if ! $music_running; then + echo " (none detected)" + fi + + echo "" + if $focus_running && $music_running; then + echo "⚠️ CONFLICT: Focus app and music running together!" + echo " Music would be killed in monitoring mode." + elif $focus_running; then + echo "✓ Focus mode active (no music playing)" + elif $music_running; then + echo "✓ Music playing (no focus app detected - this is fine)" + else + echo "✓ Idle (nothing detected)" + fi +} + +# Show usage +show_usage() { + echo "Music Parallelism Prevention Script" + echo "====================================" + echo "" + echo "Usage: $0 [command]" + echo "" + echo "Commands:" + echo " monitor - Start monitoring (default, checks every ${CHECK_INTERVAL}s)" + echo " instant - Instant monitoring (${FAST_CHECK_INTERVAL}s active / ${IDLE_CHECK_INTERVAL}s idle)" + echo " status - Show current status of focus apps and music services" + echo " kill - Immediately kill all music services" + echo " help - Show this help message" + echo "" + echo "Description:" + echo " This script prevents multitasking between focus work and music." + echo " When a focus application (VS Code, Steam, etc.) is detected" + echo " alongside a music streaming service, the music is stopped." + echo "" + echo " Music is allowed when no focus apps are running." + echo "" +} diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/music_parallelism.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/music_parallelism.sh index 2797eda6..a53d7523 100755 --- a/linux_configuration/scripts/periodic_background/digital_wellbeing/music_parallelism.sh +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/music_parallelism.sh @@ -99,288 +99,11 @@ readonly IDLE_CHECK_INTERVAL=30 MUSIC_FOUND_PROCESS=0 MUSIC_FOUND_WINDOW=0 -wait_seconds() { - local timeout_s=$1 - local start_ts end_ts elapsed_s remaining_s - - if [[ -n ${MUSIC_PARALLELISM_TEST_WAIT_LOG:-} ]]; then - printf '%s\n' "$timeout_s" >>"$MUSIC_PARALLELISM_TEST_WAIT_LOG" - if [[ ${MUSIC_PARALLELISM_TEST_EXIT_AFTER_WAIT:-0} -eq 1 ]]; then - exit 99 - fi - return 0 - fi - - 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 -} - -contains_music_process() { - local comm_file comm_lower token_lower - - for comm_file in "$PROC_ROOT"/[0-9]*/comm; do - [[ -r $comm_file ]] || continue - read -r comm_lower <"$comm_file" || continue - comm_lower=${comm_lower,,} - - for token_lower in "${MUSIC_PROCESS_NAMES[@]}"; do - if [[ $comm_lower == *"${token_lower,,}"* ]]; then - return 0 - fi - done - done - - return 1 -} - -# Check if any music service is running and return its details (OPTIMIZED: batch pgrep calls) -find_music_services() { - local found_services=() - MUSIC_FOUND_PROCESS=0 - MUSIC_FOUND_WINDOW=0 - - # Check processes using /proc (fork-free) - if contains_music_process; then - MUSIC_FOUND_PROCESS=1 - found_services+=("music process") - fi - - # Check windows (use optimized is_focus_app_running logic: single xdotool regex call) - if command -v xdotool &>/dev/null && [[ ${#MUSIC_SERVICES[@]} -gt 0 ]]; then - if xdotool search --name "$MUSIC_WINDOWS_PATTERN" &>/dev/null 2>&1; then - MUSIC_FOUND_WINDOW=1 - found_services+=("music service (window)") - fi - fi - - if [[ ${#found_services[@]} -gt 0 ]]; then - printf '%s\n' "${found_services[@]}" - return 0 - fi - return 1 -} - -# Kill music services -kill_music_services() { - local use_cached_detection="${1:-0}" - local killed=false - local window_pattern='YouTube Music|music\.youtube\.com|music\.apple\.com|soundcloud\.com|pandora\.com|deezer\.com|tidal\.com' - local should_check_windows=1 - local should_check_processes=1 - - if [[ $use_cached_detection -eq 1 ]]; then - should_check_windows=$MUSIC_FOUND_WINDOW - should_check_processes=$MUSIC_FOUND_PROCESS - fi - - # Close browser tabs for web-based music services via one xdotool search - if [[ $should_check_windows -eq 1 ]] && command -v xdotool &>/dev/null; then - local windows wid - windows=$(xdotool search --name "$window_pattern" 2>/dev/null || true) - for wid in $windows; do - [[ -n $wid ]] || continue - xdotool windowclose "$wid" 2>/dev/null || true - killed=true - done - fi - - # Kill app processes with /proc scan + builtin kill (fork-free in hot path) - if [[ $should_check_processes -eq 1 ]]; then - local comm_file pid comm_lower token_lower - for comm_file in "$PROC_ROOT"/[0-9]*/comm; do - [[ -r $comm_file ]] || continue - read -r comm_lower <"$comm_file" || continue - comm_lower=${comm_lower,,} - pid=${comm_file#"$PROC_ROOT"/} - pid=${pid%%/*} - - for token_lower in "${MUSIC_PROCESS_NAMES[@]}"; do - if [[ $comm_lower == *"${token_lower,,}"* ]]; then - if [[ $PROC_ROOT != "/proc" ]]; then - # Test mode (fake proc tree): mark as killed without signaling host PIDs. - killed=true - elif kill -9 "$pid" 2>/dev/null; then - killed=true - fi - break - fi - done - done - fi - - if $killed; then - return 0 - fi - return 1 -} - -# Send notification to user -notify_user() { - local focus_app="$1" - local message="Music stopped - focus mode active ($focus_app detected)" - - # Try to send desktop notification - if command -v notify-send &>/dev/null; then - notify-send -u normal -t 5000 "🎵 Music Parallelism" "$message" 2>/dev/null || true - fi - - log_message "$message" -} - -# Instant monitoring loop - uses polling at high frequency ONLY when focus app is detected -# When focus app active: checks every 0.5s. When idle: checks every 3s. Reduces fork overhead. -# OPTIMIZATION: Single batched pgrep call instead of multiple separate calls -instant_monitor_loop() { - local next_enforcement_ts=0 - local current_ts=0 - local focus_app="" - local sleep_interval="$IDLE_CHECK_INTERVAL" - - log_message "=== Music Parallelism INSTANT Monitor Started ===" - log_message "Focus apps (windows): ${FOCUS_APPS_WINDOWS[*]}" - log_message "Focus apps (processes): ${FOCUS_APPS_PROCESSES[*]}" - log_message "Polling: ${FAST_CHECK_INTERVAL}s active, ${ACTIVE_NO_MUSIC_INTERVAL}s stable-focus, ${IDLE_CHECK_INTERVAL}s idle, ${ENFORCEMENT_COOLDOWN}s enforcement cooldown" - - while true; do - if focus_app=$(is_focus_app_running 2>/dev/null); then - current_ts=$(get_timestamp) - if ((current_ts >= next_enforcement_ts)); then - if find_music_services >/dev/null 2>&1; then - if kill_music_services 1; then - notify_user "$focus_app" - log_message "INSTANT KILL: Music services terminated" - sleep_interval="$ACTIVE_AFTER_KILL_INTERVAL" - fi - else - sleep_interval="$ACTIVE_NO_MUSIC_INTERVAL" - fi - next_enforcement_ts=$((current_ts + ENFORCEMENT_COOLDOWN)) - else - sleep_interval="$ACTIVE_NO_MUSIC_INTERVAL" - fi - else - next_enforcement_ts=0 - sleep_interval="$IDLE_CHECK_INTERVAL" - fi - - wait_seconds "$sleep_interval" - done -} - -# Main monitoring loop -monitor_loop() { - log_message "=== Music Parallelism Monitor Started ===" - log_message "Focus apps (windows): ${FOCUS_APPS_WINDOWS[*]}" - log_message "Focus apps (processes): ${FOCUS_APPS_PROCESSES[*]}" - log_message "Music services monitored: ${MUSIC_SERVICES[*]}" - log_message "Check interval: ${CHECK_INTERVAL}s" - - while true; do - # Check if a focus app is running - local focus_app - if focus_app=$(is_focus_app_running); then - # Focus app detected, check for music services - local music_services - if music_services=$(find_music_services); then - log_message "Conflict detected: Focus app '$focus_app' running with music services" - log_message "Active music services: $music_services" - - # Kill the music services - if kill_music_services 1; then - notify_user "$focus_app" - fi - fi - fi - - wait_seconds "$CHECK_INTERVAL" - done -} - -# Show status -show_status() { - echo "Music Parallelism Monitor Status" - echo "=================================" - echo "" - - echo "Focus Applications (window-based detection):" - local focus_running=false - - # Check windows (OPTIMIZED: single xdotool call with combined regex) - if command -v xdotool &>/dev/null && [[ ${#FOCUS_APPS_WINDOWS[@]} -gt 0 ]]; then - local regex - printf -v regex '%s|' "${FOCUS_APPS_WINDOWS[@]}" - regex="${regex%|}" # strip trailing | - if xdotool search --name "$regex" &>/dev/null 2>&1; then - echo " ✓ Focus window detected" - focus_running=true - fi - fi - - # Check processes using shared /proc-based helper (fork-free) - if is_focus_app_running >/dev/null 2>&1; then - echo " ✓ Focus process running" - focus_running=true - fi - - if ! $focus_running; then - echo " (none detected)" - fi - - echo "" - echo "Music Services:" - local music_running=false - if music_services=$(find_music_services 2>/dev/null); then - echo "$music_services" | while read -r svc; do - echo " ♪ $svc (RUNNING)" - done - music_running=true - fi - if ! $music_running; then - echo " (none detected)" - fi - - echo "" - if $focus_running && $music_running; then - echo "⚠️ CONFLICT: Focus app and music running together!" - echo " Music would be killed in monitoring mode." - elif $focus_running; then - echo "✓ Focus mode active (no music playing)" - elif $music_running; then - echo "✓ Music playing (no focus app detected - this is fine)" - else - echo "✓ Idle (nothing detected)" - fi -} - -# Show usage -show_usage() { - echo "Music Parallelism Prevention Script" - echo "====================================" - echo "" - echo "Usage: $0 [command]" - echo "" - echo "Commands:" - echo " monitor - Start monitoring (default, checks every ${CHECK_INTERVAL}s)" - echo " instant - Instant monitoring (${FAST_CHECK_INTERVAL}s active / ${IDLE_CHECK_INTERVAL}s idle)" - echo " status - Show current status of focus apps and music services" - echo " kill - Immediately kill all music services" - echo " help - Show this help message" - echo "" - echo "Description:" - echo " This script prevents multitasking between focus work and music." - echo " When a focus application (VS Code, Steam, etc.) is detected" - echo " alongside a music streaming service, the music is stopped." - echo "" - echo " Music is allowed when no focus apps are running." - echo "" -} +# Sourced after the configuration above, which they read. music_detect.sh comes +# first: the monitor loops call its helpers. +source "$SCRIPT_DIR/lib/music_detect.sh" +source "$SCRIPT_DIR/lib/music_monitor.sh" +source "$SCRIPT_DIR/lib/music_status.sh" # Main case "${1:-instant}" in diff --git a/linux_configuration/tests/test_music_parallelism.sh b/linux_configuration/tests/test_music_parallelism.sh index 4fd18e5d..9a7a83d3 100755 --- a/linux_configuration/tests/test_music_parallelism.sh +++ b/linux_configuration/tests/test_music_parallelism.sh @@ -49,6 +49,11 @@ WORKTREE_LIB="$WORKTREE/scripts/lib/common.sh" mkdir -p "$(dirname "$WORKTREE_SCRIPT")" "$(dirname "$WORKTREE_LIB")" "$BIN_DIR" cp "$TARGET_SCRIPT" "$WORKTREE_SCRIPT" +# The script sources its own libs from $SCRIPT_DIR/lib/. They must travel into +# the worktree beside it, or every source line fails and the script exits before +# any behaviour under test runs. +cp -r "$(dirname "$TARGET_SCRIPT")/lib" "$(dirname "$WORKTREE_SCRIPT")/lib" + # Fail loudly if the copy cannot reach the stub, rather than letting the script # fall through to the real library and produce a misleading behavioural failure. STUB_FROM_SCRIPT="$(dirname "$WORKTREE_SCRIPT")/../../lib/common.sh"