Files
SDG-OS/sdgos/wayshell/wayshell.sh
T
2026-06-27 10:50:50 +02:00

267 lines
9.6 KiB
Bash
Executable File

#!/bin/bash
#===============================================================================
# Wayshell Daemon — Module Event Manager
#===============================================================================
# Description:
# Reads module configuration, spawns zone/layout/focused event sources,
# and dispatches ON/OFF actions with configurable trailing-edge debounce.
#
# Dependencies: jq, mmsg
# Config: wayshell.conf (zone_buffer, on_delay, off_delay)
# Modules: wayshell.modules (name,on_exec,off_exec,type,args...)
#===============================================================================
CONFIG_DIR="${HOME}/.config/sdgos/wayshell"
CONFIG_FILE="${CONFIG_DIR}/wayshell.conf"
MODULES_FILE="${CONFIG_DIR}/wayshell.modules"
MODULE_DIR="${CONFIG_DIR}/modules"
exec 1>>/tmp/wayshell_daemon.log 2>&1
echo "=== Starting Wayshell Daemon ==="
CLEANED_UP=false
cleanup() {
$CLEANED_UP && return
CLEANED_UP=true
echo "Shutting down Wayshell Daemon..."
kill -- -$$ 2>/dev/null
exit 0
}
trap cleanup SIGTERM SIGINT EXIT
# --- Config ---
ON_DELAY=$(grep -oP 'on_delay=\K[0-9]+' "$CONFIG_FILE" 2>/dev/null || echo "100")
OFF_DELAY=$(grep -oP 'off_delay=\K[0-9]+' "$CONFIG_FILE" 2>/dev/null || echo "100")
echo "Config: on_delay=$ON_DELAY, off_delay=$OFF_DELAY"
# --- Storage ---
declare -A MODULES
declare -A MODULE_STATES
declare -A MODULE_ENTER_TS
declare -A MODULE_EXIT_TS
# --- Module parsing ---
parse_modules() {
echo "Parsing modules from $MODULES_FILE"
[[ -f "$MODULES_FILE" ]] || { echo "ERROR: Modules file not found"; exit 1; }
local count=0
while IFS=',' read -r name onexec offexec type args; do
name="${name// /}"
[[ "$name" =~ ^#.*$ || -z "$name" ]] && continue
MODULES["$name"]="$onexec,$offexec,$type,$args"
MODULE_STATES["$name"]="disabled"
((count++))
echo " [$count] $name ($type)"
done < "$MODULES_FILE"
echo "Total modules loaded: $count"
}
parse_modules
# --- Filter ---
modules_by_type() {
local t="$1"
for n in "${!MODULES[@]}"; do
IFS=',' read -r _ _ mt _ <<< "${MODULES[$n]}"
[[ "$mt" == "$t" ]] && echo "$n"
done
}
# --- Monitor offset cache ---
declare -A MONITOR_OFFSETS
MONITOR_CACHE_TS=0
get_monitor_offset() {
local mon="$1"; local now; now=$(date +%s)
if (( now - MONITOR_CACHE_TS > 5 )); then
local json entry name ox oy
json=$(mmsg get all-monitors 2>/dev/null)
if [[ -n "$json" ]]; then
while IFS= read -r entry; do
name=$(jq -r '.name' <<< "$entry" 2>/dev/null)
ox=$(jq -r '.x' <<< "$entry" 2>/dev/null); oy=$(jq -r '.y' <<< "$entry" 2>/dev/null)
[[ -n "$name" && "$ox" != "null" ]] && MONITOR_OFFSETS["$name"]="$ox,$oy"
done < <(jq -c '.monitors[]' <<< "$json" 2>/dev/null)
fi
MONITOR_CACHE_TS=$now
fi
echo "${MONITOR_OFFSETS[$mon]:-0,0}"
}
# --- Check debounce timers and fire actions ---
# Called every ~50ms from the main event loop.
check_fires() {
local now name onexec offexec
now=$(date +%s%3N)
# Pending ON fires
for name in "${!MODULE_ENTER_TS[@]}"; do
local st=${MODULE_STATES[$name]}
[[ "$st" != "pending_on" && "$st" != "pending_off" ]] && continue
if (( now - MODULE_ENTER_TS[$name] >= ON_DELAY )); then
if [[ "$st" == "pending_on" ]]; then
MODULE_STATES["$name"]="enabled"
IFS=',' read -r onexec _ _ _ <<< "${MODULES[$name]}"
echo "ON $name"
eval "$onexec" &
fi
unset "MODULE_ENTER_TS[$name]"
fi
done
# Pending OFF fires
for name in "${!MODULE_EXIT_TS[@]}"; do
local st=${MODULE_STATES[$name]}
[[ "$st" != "pending_off" && "$st" != "enabled" ]] && continue
if (( now - MODULE_EXIT_TS[$name] >= OFF_DELAY )); then
MODULE_STATES["$name"]="disabled"
IFS=',' read -r _ offexec _ _ <<< "${MODULES[$name]}"
echo "OFF $name"
eval "$offexec" &
unset "MODULE_EXIT_TS[$name]"
fi
done
}
# --- Process zone events ---
process_zone_event() {
local data="$1"
# Exit event — cursor left the edge zone entirely
local state
state=$(jq -r '.state // "enter"' <<< "$data" 2>/dev/null)
if [[ "$state" == "exit" ]]; then
local now; now=$(date +%s%3N)
while IFS= read -r module_name; do
[[ -z "$module_name" ]] && continue
local st=${MODULE_STATES[$module_name]}
if [[ "$st" == "enabled" || "$st" == "pending_on" ]]; then
MODULE_EXIT_TS[$module_name]=$now
MODULE_STATES["$module_name"]="pending_off"
unset "MODULE_ENTER_TS[$module_name]"
fi
done < <(modules_by_type "zone")
return
fi
# Enter event — check bounding boxes
local x y monitor
x=$(jq -r '.x' <<< "$data" 2>/dev/null)
y=$(jq -r '.y' <<< "$data" 2>/dev/null)
monitor=$(jq -r '.monitor' <<< "$data" 2>/dev/null)
[[ -z "$x" || -z "$y" || -z "$monitor" ]] && return
local x_int=${x%.*}
local y_int=${y%.*}
local offset mx my
offset=$(get_monitor_offset "$monitor")
mx="${offset%,*}"; my="${offset#*,}"
local now; now=$(date +%s%3N)
while IFS= read -r module_name; do
[[ -z "$module_name" ]] && continue
IFS=',' read -r onexec offexec _ args <<< "${MODULES[$module_name]}"
IFS=',' read -r x1 y1 x2 y2 <<< "$args"
local ax1=$(( x1 + mx )) ay1=$(( y1 + my ))
local ax2=$(( x2 + mx )) ay2=$(( y2 + my ))
local in=$(( x_int >= ax1 && x_int <= ax2 && y_int >= ay1 && y_int <= ay2 ? 1 : 0 ))
local st=${MODULE_STATES[$module_name]}
if (( in )); then
if [[ "$st" == "disabled" ]]; then
MODULE_ENTER_TS[$module_name]=$now
MODULE_STATES["$module_name"]="pending_on"
elif [[ "$st" == "pending_off" ]]; then
# Re-entered before off delay — cancel OFF, stay enabled
unset "MODULE_EXIT_TS[$module_name]"
MODULE_STATES["$module_name"]="enabled"
fi
else
if [[ "$st" == "enabled" || "$st" == "pending_on" ]]; then
MODULE_EXIT_TS[$module_name]=$now
MODULE_STATES["$module_name"]="pending_off"
unset "MODULE_ENTER_TS[$module_name]"
fi
fi
done < <(modules_by_type "zone")
}
# --- Layout event processing ---
process_layout_event() {
local event="$1"; local layout state event_monitor
layout=$(jq -r '.layout' <<< "$event" 2>/dev/null)
state=$(jq -r '.state' <<< "$event" 2>/dev/null)
event_monitor=$(jq -r '.monitor // empty' <<< "$event" 2>/dev/null)
[[ -z "$layout" || -z "$state" ]] && return
local now; now=$(date +%s%3N)
while IFS= read -r module_name; do
[[ -z "$module_name" ]] && continue
IFS=',' read -r onexec offexec _ args <<< "${MODULES[$module_name]}"
# args = "layout_name" or "layout_name,monitor_name"
local expected_layout="${args%%,*}"
local expected_monitor=""
[[ "$args" == *","* ]] && expected_monitor="${args#*,}"
[[ "$layout" != "$expected_layout" ]] && continue
[[ -n "$expected_monitor" && "$event_monitor" != "$expected_monitor" ]] && continue
local st=${MODULE_STATES[$module_name]}
if [[ "$state" == "active" && "$st" == "disabled" ]]; then
MODULE_ENTER_TS[$module_name]=$now
MODULE_STATES["$module_name"]="pending_on"
elif [[ "$state" == "inactive" && ( "$st" == "enabled" || "$st" == "pending_on" ) ]]; then
MODULE_EXIT_TS[$module_name]=$now
MODULE_STATES["$module_name"]="pending_off"
unset "MODULE_ENTER_TS[$module_name]"
fi
done < <(modules_by_type "layout")
}
# --- Focused event processing ---
process_focused_event() {
local event="$1"; local app_id state
app_id=$(jq -r '.app_id' <<< "$event" 2>/dev/null)
state=$(jq -r '.state' <<< "$event" 2>/dev/null)
[[ -z "$app_id" || -z "$state" ]] && return
local now; now=$(date +%s%3N)
while IFS= read -r module_name; do
[[ -z "$module_name" ]] && continue
IFS=',' read -r onexec offexec _ expected <<< "${MODULES[$module_name]}"
[[ "$app_id" != "$expected" ]] && continue
local st=${MODULE_STATES[$module_name]}
if [[ "$state" == "focused" && "$st" == "disabled" ]]; then
MODULE_ENTER_TS[$module_name]=$now
MODULE_STATES["$module_name"]="pending_on"
elif [[ "$state" == "unfocused" && ( "$st" == "enabled" || "$st" == "pending_on" ) ]]; then
MODULE_EXIT_TS[$module_name]=$now
MODULE_STATES["$module_name"]="pending_off"
unset "MODULE_ENTER_TS[$module_name]"
fi
done < <(modules_by_type "focused")
}
# --- Start module subprocesses (auto-restarting) ---
echo "Starting modules..."
(
start_src() {
local script="$1" label="$2"
[[ -x "$script" ]] || { echo "WARNING: $script not found — $label disabled" >&2; return; }
(
while true; do "$script"; sleep 0.5; done
) | while IFS= read -r line; do echo "${label}:$line"; done &
}
start_src "$MODULE_DIR/zone.sh" "zone"
start_src "$MODULE_DIR/layout.sh" "layout"
start_src "$MODULE_DIR/focused.sh" "focused"
wait
) | while true; do
if IFS= read -t 0.05 -r line; then
source="${line%%:*}"
data="${line#*:}"
case "$source" in
zone) process_zone_event "$data" ;;
layout) process_layout_event "$data" ;;
focused) process_focused_event "$data" ;;
esac
fi
check_fires
done