From 79b45ab123db37fa2ec7e80feae5b3fc35cd7a80 Mon Sep 17 00:00:00 2001 From: SDGDen Date: Sat, 27 Jun 2026 10:50:50 +0200 Subject: [PATCH] added wayshell framework --- matugen/config.toml | 4 + sdgos/wayshell/AGENTS.md | 38 ++++ sdgos/wayshell/GUIDELINES.md | 38 ++++ sdgos/wayshell/PLAN.md | 50 +++++ sdgos/wayshell/colors.css | 105 +++++++++++ sdgos/wayshell/modules/focused.sh | 45 +++++ sdgos/wayshell/modules/layout.sh | 68 +++++++ sdgos/wayshell/modules/zone.sh | 103 +++++++++++ sdgos/wayshell/style.css | 52 ++++++ sdgos/wayshell/temp.md | 22 +++ sdgos/wayshell/tests/run_all.sh | 28 +++ sdgos/wayshell/tests/test_focused.sh | 63 +++++++ sdgos/wayshell/tests/test_layout.sh | 67 +++++++ sdgos/wayshell/tests/test_parsing.sh | 73 ++++++++ sdgos/wayshell/tests/test_zone.sh | 65 +++++++ sdgos/wayshell/wayshell.conf | 8 + sdgos/wayshell/wayshell.modules | 30 +++ sdgos/wayshell/wayshell.sh | 266 +++++++++++++++++++++++++++ 18 files changed, 1125 insertions(+) create mode 100644 sdgos/wayshell/AGENTS.md create mode 100644 sdgos/wayshell/GUIDELINES.md create mode 100644 sdgos/wayshell/PLAN.md create mode 100644 sdgos/wayshell/colors.css create mode 100755 sdgos/wayshell/modules/focused.sh create mode 100755 sdgos/wayshell/modules/layout.sh create mode 100755 sdgos/wayshell/modules/zone.sh create mode 100644 sdgos/wayshell/style.css create mode 100644 sdgos/wayshell/temp.md create mode 100755 sdgos/wayshell/tests/run_all.sh create mode 100755 sdgos/wayshell/tests/test_focused.sh create mode 100755 sdgos/wayshell/tests/test_layout.sh create mode 100755 sdgos/wayshell/tests/test_parsing.sh create mode 100755 sdgos/wayshell/tests/test_zone.sh create mode 100644 sdgos/wayshell/wayshell.conf create mode 100644 sdgos/wayshell/wayshell.modules create mode 100755 sdgos/wayshell/wayshell.sh diff --git a/matugen/config.toml b/matugen/config.toml index 207bd98..aff7eb8 100755 --- a/matugen/config.toml +++ b/matugen/config.toml @@ -82,3 +82,7 @@ output_path = '~/.config/sdgos/monocle/colors.css' [templates.caffyne] input_path = '~/.config/caffyne-shell/matugen/caffyne-shell-colors.css' output_path = '~/.config/caffyne-shell/style/colors.css' + +[templates.wayshell] +input_path = '~/.config/matugen/templates/InioX/colors.css' +output_path = '~/.config/sdgos/wayshell/colors.css' \ No newline at end of file diff --git a/sdgos/wayshell/AGENTS.md b/sdgos/wayshell/AGENTS.md new file mode 100644 index 0000000..3d359c7 --- /dev/null +++ b/sdgos/wayshell/AGENTS.md @@ -0,0 +1,38 @@ +# Wayshell Module System Agent Instructions + +## Overview + +This document provides instructions for the Wayshell Module System agent. The agent is responsible for managing the different module types and handling events. + +## Module Types + +### Zone Module + +- **Initialization**: Initialize the Zone module by parsing the configuration files. +- **Event Handling**: Handle events for entering and exiting zones. + +### Layout Module + +- **Initialization**: Initialize the Layout module by parsing the configuration files. +- **Event Handling**: Handle events for activating and deactivating layouts. + +### Focused Module + +- **Initialization**: Initialize the Focused module by parsing the configuration files. +- **Event Handling**: Handle events for focusing and unfocusing clients. + +## Configuration Files + +- **wayshell.conf**: General configuration file. +- **wayshell.modules**: Defines the modules. + +## Performance Optimization + +- **Efficient Parsing**: Optimize the parsing of configuration files to minimize CPU usage. +- **Event Debouncing**: Implement debouncing for events to avoid unnecessary triggers. + +## Testing + +- **Unit Tests**: Write unit tests for each module type. +- **Integration Tests**: Write integration tests to ensure the modules work together correctly. +- **Performance Tests**: Conduct performance tests to ensure the system meets the CPU usage constraints. \ No newline at end of file diff --git a/sdgos/wayshell/GUIDELINES.md b/sdgos/wayshell/GUIDELINES.md new file mode 100644 index 0000000..a532209 --- /dev/null +++ b/sdgos/wayshell/GUIDELINES.md @@ -0,0 +1,38 @@ +# Wayshell Module System Design Guidelines + +## Overview + +The Wayshell Module System is designed to be flexible, efficient, and easy to use. This document outlines the design guidelines for the system. + +## Module Types + +### Zone Module + +- **Definition**: Allows defining zones and triggers for entering and exiting these zones. +- **Implementation**: Use efficient data structures to manage zones and triggers. + +### Layout Module + +- **Definition**: Allows triggering actions based on the activation or deactivation of specific layout types. +- **Implementation**: Use event-driven architecture to handle layout changes. + +### Focused Module + +- **Definition**: Allows triggering actions based on focusing or unfocusing specific client types. +- **Implementation**: Use efficient event handling to manage focus changes. + +## Configuration Files + +- **wayshell.conf**: General configuration file. +- **wayshell.modules**: Defines the modules. + +## Performance Optimization + +- **Efficient Parsing**: Optimize the parsing of configuration files to minimize CPU usage. +- **Event Debouncing**: Implement debouncing for events to avoid unnecessary triggers. + +## Testing + +- **Unit Tests**: Write unit tests for each module type. +- **Integration Tests**: Write integration tests to ensure the modules work together correctly. +- **Performance Tests**: Conduct performance tests to ensure the system meets the CPU usage constraints. \ No newline at end of file diff --git a/sdgos/wayshell/PLAN.md b/sdgos/wayshell/PLAN.md new file mode 100644 index 0000000..490eabe --- /dev/null +++ b/sdgos/wayshell/PLAN.md @@ -0,0 +1,50 @@ +# Wayshell Module System Plan + +## Overview + +The Wayshell Module System is designed to provide a flexible and efficient way to manage different types of modules in a Wayland environment. The system will parse configuration files and trigger actions based on specific events. + +## Module Types + +1. **Zone Module**: Allows defining zones and triggers for entering and exiting these zones. +2. **Layout Module**: Allows triggering actions based on the activation or deactivation of specific layout types. +3. **Focused Module**: Allows triggering actions based on focusing or unfocusing specific client types. + +## Configuration Files + +- **wayshell.conf**: General configuration file. +- **wayshell.modules**: Defines the modules. + +## Design Constraints + +- Minimize CPU usage to less than 3% when running as a daemon. +- Avoid constantly running expensive commands. + +## Implementation Plan + +### Phase 1: Initial Setup + +1. **Parse Configuration Files**: Implement parsers for `wayshell.conf` and `wayshell.modules`. +2. **Module Initialization**: Initialize the different module types (Zone, Layout, Focused). + +### Phase 2: Event Handling + +1. **Zone Module**: Implement event triggers for entering and exiting zones. +2. **Layout Module**: Implement event triggers for activating and deactivating layouts. +3. **Focused Module**: Implement event triggers for focusing and unfocusing clients. + +### Phase 3: Performance Optimization + +1. **Efficient Parsing**: Optimize the parsing of configuration files to minimize CPU usage. +2. **Event Debouncing**: Implement debouncing for events to avoid unnecessary triggers. + +### Phase 4: Testing + +1. **Unit Tests**: Write unit tests for each module type. +2. **Integration Tests**: Write integration tests to ensure the modules work together correctly. +3. **Performance Tests**: Conduct performance tests to ensure the system meets the CPU usage constraints. + +## Next Steps + +1. Review the plan and provide feedback. +2. Proceed with the implementation based on the approved plan. \ No newline at end of file diff --git a/sdgos/wayshell/colors.css b/sdgos/wayshell/colors.css new file mode 100644 index 0000000..b29fb9c --- /dev/null +++ b/sdgos/wayshell/colors.css @@ -0,0 +1,105 @@ +/* +* Css Colors +* Generated with Matugen +*/ + + @define-color background #0d141c; + + @define-color error #ffb4ab; + + @define-color error_container #93000a; + + @define-color inverse_on_surface #2a3139; + + @define-color inverse_primary #0062a0; + + @define-color inverse_surface #dde3ee; + + @define-color on_background #dde3ee; + + @define-color on_error #690005; + + @define-color on_error_container #ffdad6; + + @define-color on_primary #003256; + + @define-color on_primary_container #d0e4ff; + + @define-color on_primary_fixed #001d35; + + @define-color on_primary_fixed_variant #00497a; + + @define-color on_secondary #22304c; + + @define-color on_secondary_container #d8e2ff; + + @define-color on_secondary_fixed #0c1b36; + + @define-color on_secondary_fixed_variant #394664; + + @define-color on_surface #dde3ee; + + @define-color on_surface_variant #c0c7d2; + + @define-color on_tertiary #282c5a; + + @define-color on_tertiary_container #e0e0ff; + + @define-color on_tertiary_fixed #121644; + + @define-color on_tertiary_fixed_variant #3e4372; + + @define-color outline #8b919b; + + @define-color outline_variant #414750; + + @define-color primary #9bcaff; + + @define-color primary_container #00497a; + + @define-color primary_fixed #d0e4ff; + + @define-color primary_fixed_dim #9bcaff; + + @define-color scrim #000000; + + @define-color secondary #b8c6ea; + + @define-color secondary_container #394664; + + @define-color secondary_fixed #d8e2ff; + + @define-color secondary_fixed_dim #b8c6ea; + + @define-color shadow #000000; + + @define-color source_color #6c8cb0; + + @define-color surface #0d141c; + + @define-color surface_bright #333a42; + + @define-color surface_container #192028; + + @define-color surface_container_high #242b33; + + @define-color surface_container_highest #2f353e; + + @define-color surface_container_low #151c24; + + @define-color surface_container_lowest #080f16; + + @define-color surface_dim #0d141c; + + @define-color surface_tint #9bcaff; + + @define-color surface_variant #414750; + + @define-color tertiary #bfc2fa; + + @define-color tertiary_container #3e4372; + + @define-color tertiary_fixed #e0e0ff; + + @define-color tertiary_fixed_dim #bfc2fa; + diff --git a/sdgos/wayshell/modules/focused.sh b/sdgos/wayshell/modules/focused.sh new file mode 100755 index 0000000..ea1dae2 --- /dev/null +++ b/sdgos/wayshell/modules/focused.sh @@ -0,0 +1,45 @@ +#!/bin/bash +#=============================================================================== +# Focused Module — Detect client focus/unfocus events +#=============================================================================== +# Description: +# Subscribes to `mmsg watch focusing-client` and emits JSON when the focused +# client changes. Tracks the previously focused app_id to emit unfocused +# events. +# +# Output format (JSON lines): +# {"app_id":"firefox","state":"focused"} +# {"app_id":"firefox","state":"unfocused"} +# {"app_id":"com.mitchellh.ghostty","state":"focused"} +# +# Dependencies: +# - jq +# - mmsg +#=============================================================================== + +PREV_APP_ID="" + +if ! command -v mmsg &>/dev/null || ! command -v jq &>/dev/null; then + echo "focused.sh: mmsg and jq are required" >&2 + exit 1 +fi + +while read -r line; do + app_id=$(jq -r '.appid // empty' <<< "$line" 2>/dev/null) + + if [[ -z "$app_id" || "$app_id" == "null" ]]; then + if [[ -n "$PREV_APP_ID" ]]; then + echo "{\"app_id\":\"$PREV_APP_ID\",\"state\":\"unfocused\"}" + PREV_APP_ID="" + fi + continue + fi + + if [[ "$app_id" != "$PREV_APP_ID" ]]; then + if [[ -n "$PREV_APP_ID" ]]; then + echo "{\"app_id\":\"$PREV_APP_ID\",\"state\":\"unfocused\"}" + fi + echo "{\"app_id\":\"$app_id\",\"state\":\"focused\"}" + PREV_APP_ID="$app_id" + fi +done < <(mmsg watch focusing-client 2>/dev/null) diff --git a/sdgos/wayshell/modules/layout.sh b/sdgos/wayshell/modules/layout.sh new file mode 100755 index 0000000..254f5b4 --- /dev/null +++ b/sdgos/wayshell/modules/layout.sh @@ -0,0 +1,68 @@ +#!/bin/bash +#=============================================================================== +# Layout Module — Detect layout activation/deactivation events +#=============================================================================== +# Description: +# Subscribes to `mmsg watch all-tags` and emits JSON when any tag's layout +# becomes active or inactive on any monitor. +# +# Output format (JSON lines): +# {"layout":"M","state":"active","monitor":"DP-1","tag":1} +# {"layout":"M","state":"inactive","monitor":"DP-1","tag":1} +# +# Dependencies: +# - jq +# - mmsg +#=============================================================================== + +declare -A ACTIVE_LAYOUTS + +make_key() { + echo "${1}:${2}:${3}" +} + +process_tags_update() { + local json="$1" + local monitors monitor tag layout is_active key new_keys="" + + monitors=$(jq -c '.all_tags[]' <<< "$json" 2>/dev/null) + + while IFS= read -r entry; do + [[ -z "$entry" ]] && continue + monitor=$(jq -r '.monitor' <<< "$entry" 2>/dev/null) + [[ -z "$monitor" ]] && continue + + while IFS= read -r tag_entry; do + [[ -z "$tag_entry" ]] && continue + tag=$(jq -r '.index' <<< "$tag_entry" 2>/dev/null) + layout=$(jq -r '.layout' <<< "$tag_entry" 2>/dev/null) + is_active=$(jq -r '.is_active' <<< "$tag_entry" 2>/dev/null) + key=$(make_key "$monitor" "$tag" "$layout") + new_keys="$new_keys $key" + + if [[ "$is_active" == "true" ]]; then + if [[ -z "${ACTIVE_LAYOUTS[$key]}" ]]; then + echo "{\"layout\":\"$layout\",\"state\":\"active\",\"monitor\":\"$monitor\",\"tag\":$tag}" + fi + ACTIVE_LAYOUTS["$key"]="active" + fi + done < <(jq -c '.tags[]' <<< "$entry" 2>/dev/null) + done <<< "$monitors" + + for key in "${!ACTIVE_LAYOUTS[@]}"; do + if [[ ! " $new_keys " =~ $key ]]; then + IFS=':' read -r monitor tag layout <<< "$key" + echo "{\"layout\":\"$layout\",\"state\":\"inactive\",\"monitor\":\"$monitor\",\"tag\":$tag}" + unset "ACTIVE_LAYOUTS[$key]" + fi + done +} + +if ! command -v mmsg &>/dev/null || ! command -v jq &>/dev/null; then + echo "layout.sh: mmsg and jq are required" >&2 + exit 1 +fi + +while read -r line; do + process_tags_update "$line" +done < <(mmsg watch all-tags 2>/dev/null) diff --git a/sdgos/wayshell/modules/zone.sh b/sdgos/wayshell/modules/zone.sh new file mode 100755 index 0000000..b6f5d5a --- /dev/null +++ b/sdgos/wayshell/modules/zone.sh @@ -0,0 +1,103 @@ +#!/bin/bash +#=============================================================================== +# Zone Detection Daemon +#=============================================================================== +# Description: +# Polls cursor position via `mmsg get cursorpos` and outputs JSON when the +# cursor is within ZONE_BUFFER pixels of any screen edge. Emits an exit +# event when the cursor leaves the edge zone, so the daemon knows the +# exact departure time for proper trailing-edge debounce. +# +# Uses monitor-relative coordinates so edge detection works on any monitor. +# +# Dependencies: jq, mmsg +# +# Output format (JSON lines): +# In zone: {"x":,"y":,"monitor":""} +# On exit: {"state":"exit","x":,"y":,"monitor":""} +#=============================================================================== + +CONFIG_FILE="${HOME}/.config/sdgos/wayshell/wayshell.conf" + +ZONE_BUFFER=$(grep -oP 'zone_buffer=\K[0-9]+' "$CONFIG_FILE" 2>/dev/null || echo "10") +ZONE_BUFFER=${ZONE_BUFFER:-10} +POLL_INTERVAL=0.1 + +was_in_zone=false +last_x="" +last_y="" +last_monitor="" + +declare -A MONITOR_CACHE +MONITOR_CACHE_AGE=0 + +get_monitor_info() { + local mon="$1" + local now + now=$(date +%s) + + if (( now - MONITOR_CACHE_AGE > 5 )) || [[ -z "${MONITOR_CACHE[$mon]}" ]]; then + local json entry name w h 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) + w=$(jq -r '.width' <<< "$entry" 2>/dev/null) + h=$(jq -r '.height' <<< "$entry" 2>/dev/null) + ox=$(jq -r '.x' <<< "$entry" 2>/dev/null) + oy=$(jq -r '.y' <<< "$entry" 2>/dev/null) + if [[ -n "$name" && "$w" != "null" && "$h" != "null" ]]; then + MONITOR_CACHE["$name"]="$w $h ${ox:-0} ${oy:-0}" + fi + done < <(jq -c '.monitors[]' <<< "$json" 2>/dev/null) + fi + MONITOR_CACHE_AGE=$now + fi + + echo "${MONITOR_CACHE[$mon]:-1920 1080 0 0}" +} + +while true; do + cursor_info=$(mmsg get cursorpos 2>/dev/null) + if [[ -z "$cursor_info" ]]; then + sleep "$POLL_INTERVAL" + continue + fi + + x=$(jq -r '.x' <<< "$cursor_info" 2>/dev/null) + y=$(jq -r '.y' <<< "$cursor_info" 2>/dev/null) + monitor=$(jq -r '.monitor' <<< "$cursor_info" 2>/dev/null) + + if [[ -z "$x" || -z "$y" || -z "$monitor" ]]; then + sleep "$POLL_INTERVAL" + continue + fi + + read -r width height m_x m_y <<< "$(get_monitor_info "$monitor")" + if [[ -z "$width" || -z "$height" ]]; then + sleep "$POLL_INTERVAL" + continue + fi + + local_x=$(( ${x%.*} - m_x )) + local_y=$(( ${y%.*} - m_y )) + + in_zone=false + (( local_x < ZONE_BUFFER )) && in_zone=true + (( local_x > width - ZONE_BUFFER )) && in_zone=true + (( local_y < ZONE_BUFFER )) && in_zone=true + (( local_y > height - ZONE_BUFFER )) && in_zone=true + + if $in_zone && ! $was_in_zone; then + echo "$cursor_info" + elif ! $in_zone && $was_in_zone; then + echo "{\"state\":\"exit\",\"x\":$last_x,\"y\":$last_y,\"monitor\":\"$last_monitor\"}" + fi + + was_in_zone=$in_zone + last_x=$x + last_y=$y + last_monitor=$monitor + + sleep "$POLL_INTERVAL" +done diff --git a/sdgos/wayshell/style.css b/sdgos/wayshell/style.css new file mode 100644 index 0000000..0539667 --- /dev/null +++ b/sdgos/wayshell/style.css @@ -0,0 +1,52 @@ +@import "./colors.css"; + +* { + font-family: "JetBrainsMono Nerd Font"; + font-weight: bold; + min-height: 0; + /* set font-size to 100% if font scaling is set to 1.00 using nwg-look */ + font-size: 97%; + font-feature-settings: '"zero", "ss01", "ss02", "ss03", "ss04", "ss05", "cv31"'; + border: 0px; + padding-top: 2px; + padding-bottom: 2px; + padding-left: 4px; + background-color: @surface; + animation: gradient_f 20s ease-in infinite; + transition: all 0.3s cubic-bezier(.55,-0.68,.48,1.682); +} + + + +#custom-daemon, +#custom-window1, +#custom-window2, +#custom-window3, +#custom-window4, +#custom-window5, +#custom-window6, +#custom-window7, +#custom-window8, +#custom-window9, +#custom-window10, +#custom-button-term, +#custom-button-files, +#custom-button-next, +#custom-button-prev, +#custom-button-zoomin, +#custom-button-zoomout, +#custom-button-fit { + background-color: @surface_container; + color: @primary; + border: 0px; + border-style: solid; + border-color: white; + border-radius: 10px; + padding-top: 4px; + padding-left: 6px; + padding-right: 6px; + padding-bottom: 4px; + +} + + diff --git a/sdgos/wayshell/temp.md b/sdgos/wayshell/temp.md new file mode 100644 index 0000000..0bb6da5 --- /dev/null +++ b/sdgos/wayshell/temp.md @@ -0,0 +1,22 @@ + + +modules: +- zone +- layout +- + + +format: + +name=exec,module,moduleargs + +eg + +monocle=waybar -c blah -s blah,layout,m + +leftzone=waybar -c blah2 -s blah2,zone,0|330|40|660 + + + + + diff --git a/sdgos/wayshell/tests/run_all.sh b/sdgos/wayshell/tests/run_all.sh new file mode 100755 index 0000000..dc21dce --- /dev/null +++ b/sdgos/wayshell/tests/run_all.sh @@ -0,0 +1,28 @@ +#!/bin/bash +#=============================================================================== +# Test runner — runs all Wayshell tests +#=============================================================================== + +DIR="$(cd "$(dirname "$0")" && pwd)" +ALL_PASS=0 +ALL_FAIL=0 + +for test_script in "$DIR"/test_*.sh; do + [[ "$(basename "$test_script")" == "run_all.sh" ]] && continue + echo "" + echo "==========================================" + echo " Running: $(basename "$test_script")" + echo "==========================================" + if bash "$test_script"; then + echo "SUITE PASSED" + else + echo "SUITE FAILED" + ((ALL_FAIL++)) + fi +done + +echo "" +echo "==========================================" +echo " Summary: $ALL_PASS suites passed, $ALL_FAIL suites failed" +echo "==========================================" +exit $ALL_FAIL diff --git a/sdgos/wayshell/tests/test_focused.sh b/sdgos/wayshell/tests/test_focused.sh new file mode 100755 index 0000000..ebe134e --- /dev/null +++ b/sdgos/wayshell/tests/test_focused.sh @@ -0,0 +1,63 @@ +#!/bin/bash +#=============================================================================== +# Unit tests: Focused module event processing logic +#=============================================================================== + +PASS=0 +FAIL=0 + +# Simulated focused module matching logic (same as wayshell.sh) +check_focused() { + local event_app="$1" event_state="$2" expected_app="$3" + [[ "$event_app" == "$expected_app" ]] && echo "$event_state" +} + +test_focused_match() { + local result + result=$(check_focused "firefox" "focused" "firefox") + [[ "$result" == "focused" ]] || { echo "FAIL: focused match"; return 1; } + echo "PASS: focused match" + return 0 +} + +test_unfocused_match() { + local result + result=$(check_focused "firefox" "unfocused" "firefox") + [[ "$result" == "unfocused" ]] || { echo "FAIL: unfocused match"; return 1; } + echo "PASS: unfocused match" + return 0 +} + +test_app_id_mismatch() { + local result + result=$(check_focused "firefox" "focused" "com.mitchellh.ghostty") + [[ -z "$result" ]] || { echo "FAIL: mismatch should be empty"; return 1; } + echo "PASS: no match when app_id differs" + return 0 +} + +test_different_apps() { + local firefox_result ghostty_result + firefox_result=$(check_focused "firefox" "focused" "firefox") + ghostty_result=$(check_focused "firefox" "focused" "ghostty") + [[ -n "$firefox_result" && -z "$ghostty_result" ]] || { echo "FAIL: different apps"; return 1; } + echo "PASS: different apps match correctly" + return 0 +} + +run_test() { + local test_name="$1" + if "$test_name"; then + ((PASS++)) + else + ((FAIL++)) + fi +} + +run_test test_focused_match +run_test test_unfocused_match +run_test test_app_id_mismatch +run_test test_different_apps + +echo "=== Results: $PASS passed, $FAIL failed ===" +exit $FAIL diff --git a/sdgos/wayshell/tests/test_layout.sh b/sdgos/wayshell/tests/test_layout.sh new file mode 100755 index 0000000..800b086 --- /dev/null +++ b/sdgos/wayshell/tests/test_layout.sh @@ -0,0 +1,67 @@ +#!/bin/bash +#=============================================================================== +# Unit tests: Layout module event processing logic +#=============================================================================== + +PASS=0 +FAIL=0 + +# Simulated layout module matching logic (same as wayshell.sh) +check_layout() { + local event_layout="$1" event_state="$2" expected_layout="$3" + [[ "$event_layout" == "$expected_layout" ]] && echo "$event_state" +} + +test_active_match() { + local result + result=$(check_layout "M" "active" "M") + [[ "$result" == "active" ]] || { echo "FAIL: active match"; return 1; } + echo "PASS: active match" + return 0 +} + +test_inactive_match() { + local result + result=$(check_layout "M" "inactive" "M") + [[ "$result" == "inactive" ]] || { echo "FAIL: inactive match"; return 1; } + echo "PASS: inactive match" + return 0 +} + +test_no_match() { + local result + result=$(check_layout "DW" "active" "M") + [[ -z "$result" ]] || { echo "FAIL: no match should be empty"; return 1; } + echo "PASS: no match when layout differs" + return 0 +} + +test_multiple_layouts() { + local matched="" + for pair in "M:active" "DW:inactive"; do + local layout="${pair%%:*}" state="${pair##*:}" + local result + result=$(check_layout "$layout" "$state" "M") + [[ -n "$result" ]] && matched="$matched $layout=$result" + done + [[ "$matched" == " M=active" ]] || { echo "FAIL: multiple layouts (got: $matched)"; return 1; } + echo "PASS: multiple layouts only match correct one" + return 0 +} + +run_test() { + local test_name="$1" + if "$test_name"; then + ((PASS++)) + else + ((FAIL++)) + fi +} + +run_test test_active_match +run_test test_inactive_match +run_test test_no_match +run_test test_multiple_layouts + +echo "=== Results: $PASS passed, $FAIL failed ===" +exit $FAIL diff --git a/sdgos/wayshell/tests/test_parsing.sh b/sdgos/wayshell/tests/test_parsing.sh new file mode 100755 index 0000000..e5be27f --- /dev/null +++ b/sdgos/wayshell/tests/test_parsing.sh @@ -0,0 +1,73 @@ +#!/bin/bash +#=============================================================================== +# Unit tests: Module parsing +#=============================================================================== + +PASS=0 +FAIL=0 + +test_parse_zone_module() { + local line="test_zone,notify-send on,notify-send off,zone,0,0,100,100" + IFS=',' read -r name onexec offexec type args <<< "$line" + [[ "$name" == "test_zone" ]] || { echo "FAIL: name"; return 1; } + [[ "$onexec" == "notify-send on" ]] || { echo "FAIL: onexec"; return 1; } + [[ "$offexec" == "notify-send off" ]] || { echo "FAIL: offexec"; return 1; } + [[ "$type" == "zone" ]] || { echo "FAIL: type=$type"; return 1; } + [[ "$args" == "0,0,100,100" ]] || { echo "FAIL: args=$args"; return 1; } + echo "PASS: parse zone module" + return 0 +} + +test_parse_layout_module() { + local line="monocle_mod,echo on,echo off,layout,M" + IFS=',' read -r name onexec offexec type args <<< "$line" + [[ "$name" == "monocle_mod" ]] || { echo "FAIL: name"; return 1; } + [[ "$type" == "layout" ]] || { echo "FAIL: type=$type"; return 1; } + [[ "$args" == "M" ]] || { echo "FAIL: args=$args"; return 1; } + echo "PASS: parse layout module" + return 0 +} + +test_parse_focused_module() { + local line="browser_focus,echo on,echo off,focused,firefox" + IFS=',' read -r name onexec offexec type args <<< "$line" + [[ "$name" == "browser_focus" ]] || { echo "FAIL: name"; return 1; } + [[ "$type" == "focused" ]] || { echo "FAIL: type=$type"; return 1; } + [[ "$args" == "firefox" ]] || { echo "FAIL: args=$args"; return 1; } + echo "PASS: parse focused module" + return 0 +} + +test_skip_comments() { + local line="# this is a comment" + local name + name=$(echo "$line" | grep -oP '^[^#,]+' || echo "") + [[ -z "$name" ]] || { echo "FAIL: comment not skipped"; return 1; } + echo "PASS: skip comments" + return 0 +} + +test_skip_empty() { + local line="" + [[ -z "$line" ]] || { echo "FAIL: empty not skipped"; return 1; } + echo "PASS: skip empty lines" + return 0 +} + +run_test() { + local test_name="$1" + if "$test_name"; then + ((PASS++)) + else + ((FAIL++)) + fi +} + +run_test test_parse_zone_module +run_test test_parse_layout_module +run_test test_parse_focused_module +run_test test_skip_comments +run_test test_skip_empty + +echo "=== Results: $PASS passed, $FAIL failed ===" +exit $FAIL diff --git a/sdgos/wayshell/tests/test_zone.sh b/sdgos/wayshell/tests/test_zone.sh new file mode 100755 index 0000000..78de3d2 --- /dev/null +++ b/sdgos/wayshell/tests/test_zone.sh @@ -0,0 +1,65 @@ +#!/bin/bash +#=============================================================================== +# Unit tests: Zone module event processing logic +#=============================================================================== + +PASS=0 +FAIL=0 + +# Simulated zone check logic (same as wayshell.sh) +check_zone() { + local x="$1" y="$2" x1="$3" y1="$4" x2="$5" y2="$6" + (( x >= x1 && x <= x2 && y >= y1 && y <= y2 )) +} + +test_within_zone() { + check_zone 5 5 0 0 30 1080 && echo "PASS: within zone" && return 0 + echo "FAIL: within zone" + return 1 +} + +test_outside_zone() { + check_zone 50 50 0 0 30 1080 && echo "FAIL: outside zone" && return 1 + echo "PASS: outside zone" + return 0 +} + +test_boundary_top_left() { + check_zone 0 0 0 0 30 1080 && echo "PASS: boundary top-left" && return 0 + echo "FAIL: boundary top-left" + return 1 +} + +test_boundary_bottom_right() { + check_zone 30 1080 0 0 30 1080 && echo "PASS: boundary bottom-right" && return 0 + echo "FAIL: boundary bottom-right" + return 1 +} + +test_multiple_zones() { + # Only left zone should match + local in_left=false in_right=false + check_zone 5 500 0 0 30 1080 && in_left=true + check_zone 5 500 1890 0 1920 1080 && in_right=true + $in_left && ! $in_right && echo "PASS: multiple zones" && return 0 + echo "FAIL: multiple zones" + return 1 +} + +run_test() { + local test_name="$1" + if "$test_name"; then + ((PASS++)) + else + ((FAIL++)) + fi +} + +run_test test_within_zone +run_test test_outside_zone +run_test test_boundary_top_left +run_test test_boundary_bottom_right +run_test test_multiple_zones + +echo "=== Results: $PASS passed, $FAIL failed ===" +exit $FAIL diff --git a/sdgos/wayshell/wayshell.conf b/sdgos/wayshell/wayshell.conf new file mode 100644 index 0000000..935e6a4 --- /dev/null +++ b/sdgos/wayshell/wayshell.conf @@ -0,0 +1,8 @@ +# Wayshell daemon configuration +# zone_buffer — distance in pixels from screen edge that defines a "zone" (default: 10) +# on_delay — debounce delay in ms before triggering ON action (default: 100) +# off_delay — debounce delay in ms before triggering OFF action (default: 100) + +zone_buffer=30 +on_delay=500 +off_delay=500 \ No newline at end of file diff --git a/sdgos/wayshell/wayshell.modules b/sdgos/wayshell/wayshell.modules new file mode 100644 index 0000000..b535efc --- /dev/null +++ b/sdgos/wayshell/wayshell.modules @@ -0,0 +1,30 @@ +# Wayshell module definitions +# Format: name,on_exec,off_exec,type,args... +# name — unique identifier for the module +# on_exec — command executed when the condition becomes true +# off_exec — command executed when the condition becomes false +# type — module type: zone | layout | focused +# +# Zone args: x1,y1,x2,y2 (screen-space bounding box) +# Layout args: layout_name[,monitor] +# layout_name: M for monocle, DW for dwindle, etc. +# monitor: optional — if set, only triggers for that display +# Focused args: app_id (e.g. firefox, com.mitchellh.ghostty) + +# Zone modules — trigger when cursor enters/exits screen edges +zone_left,notify-send "left show",notify-send "left hide",zone,0,0,30,1080 +zone_right,notify-send "right show",notify-send "right hide",zone,1890,0,1920,1080 +zone_top,notify-send "top show",notify-send "top hide",zone,0,0,1920,30 +zone_bottom,notify-send "bottom show",notify-send "bottom hide",zone,0,1050,1920,1080 + +# Layout modules — trigger when a layout is activated/deactivated on any tag +# Append a monitor name after the layout to restrict to one display: +# monocle_dp1,...,layout,M,DP-1 +#monocle,notify-send "monocle on HDMI",notify-send "monocle off HDMI",layout,M,HDMI-A-1 +#monocle_dp1,notify-send "monocle on DP-1",notify-send "monocle off DP-1",layout,M,DP-1 +#monocle_dp3,notify-send "monocle on DP-3",notify-send "monocle off DP-2",layout,M,DP-3 +#dwindle,notify-send "dwindle on",notify-send "dwindle off",layout,DW,HDMI-A-1 + +# Focused modules — trigger when specific app gains/loses focus +#term_focused,notify-send "terminal focused",notify-send "terminal unfocused",focused,com.mitchellh.ghostty +#browser_focused,notify-send "browser focused",notify-send "browser unfocused",focused,firefox diff --git a/sdgos/wayshell/wayshell.sh b/sdgos/wayshell/wayshell.sh new file mode 100755 index 0000000..445081b --- /dev/null +++ b/sdgos/wayshell/wayshell.sh @@ -0,0 +1,266 @@ +#!/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