added wayshell framework

This commit is contained in:
2026-06-27 10:50:50 +02:00
parent d4826bd336
commit 79b45ab123
18 changed files with 1125 additions and 0 deletions
+4
View File
@@ -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'
+38
View File
@@ -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.
+38
View File
@@ -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.
+50
View File
@@ -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.
+105
View File
@@ -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;
+45
View File
@@ -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)
+68
View File
@@ -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)
+103
View File
@@ -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":<float>,"y":<float>,"monitor":"<name>"}
# On exit: {"state":"exit","x":<float>,"y":<float>,"monitor":"<name>"}
#===============================================================================
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
+52
View File
@@ -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;
}
+22
View File
@@ -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
+28
View File
@@ -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
+63
View File
@@ -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
+67
View File
@@ -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
+73
View File
@@ -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
+65
View File
@@ -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
+8
View File
@@ -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
+30
View File
@@ -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
+266
View File
@@ -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