Split music_parallelism.sh into an entry script and three libs
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run

412 lines -> 135, with the nine functions in lib/music_detect.sh (133),
lib/music_monitor.sh (91) and lib/music_status.sh (85).

find_music_services and kill_music_services stay together: the first
writes MUSIC_FOUND_PROCESS/MUSIC_FOUND_WINDOW and the second reads them,
so splitting between them would thread mutable state across files.

The test suite caught what the static checks could not. The harness
copies the script into a temp worktree to stub common.sh, and copied
exactly one file -- so after the split every source line failed and the
script died before any behaviour under test ran. Teaching it to copy
lib/ too is a harness fix under decision 8: no assertion changed, the
same five checks run, exit 0 before and after.

bash -n also earned its keep: the first splice dropped notify_user's
closing brace, and shellcheck reported that only as SC1094 "parsing of
sourced file failed" against the entry.

One rename, not a suppression: monitor_loop's local `music_services`
became `active_services` after shellcheck flagged SC2153 against the
MUSIC_SERVICES config array now visible in the same file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
This commit is contained in:
Krzysztof Rudnicki 2026-08-16 21:50:34 +02:00
parent 41cc0451c9
commit d889c8e655
7 changed files with 396 additions and 282 deletions

View File

@ -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."
}

View File

@ -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."
]
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 ""
}

View File

@ -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

View File

@ -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"