feat: initail commit
This commit is contained in:
17
.env.example
Normal file
17
.env.example
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Hyprmonitors Configuration
|
||||||
|
# Copy this file to .env and modify as needed
|
||||||
|
|
||||||
|
# Server Configuration
|
||||||
|
HYPRMONITORS_HOST=0.0.0.0
|
||||||
|
HYPRMONITORS_PORT=3000
|
||||||
|
HYPRMONITORS_CORS_ENABLED=true
|
||||||
|
HYPRMONITORS_LOG_LEVEL=info
|
||||||
|
|
||||||
|
# Hyprland Configuration
|
||||||
|
HYPRMONITORS_TIMEOUT_MS=5000
|
||||||
|
HYPRMONITORS_RETRY_ATTEMPTS=3
|
||||||
|
|
||||||
|
# Logging levels: trace, debug, info, warn, error
|
||||||
|
# Host can be 0.0.0.0 (all interfaces) or 127.0.0.1 (localhost only)
|
||||||
|
# Port should be between 1024-65535
|
||||||
|
# CORS should be true for web browser access, false for API-only usage
|
||||||
62
.github/workflows/build.yaml
vendored
Normal file
62
.github/workflows/build.yaml
vendored
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
name: build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
checks-matrix:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- id: set-matrix
|
||||||
|
name: Generate Nix Matrix
|
||||||
|
run: |
|
||||||
|
set -Eeu
|
||||||
|
matrix="$(nix eval --json '.#githubActions.matrix')"
|
||||||
|
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
checks-build:
|
||||||
|
needs: checks-matrix
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix: ${{fromJSON(needs.checks-matrix.outputs.matrix)}}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- run: nix build -L '.#${{ matrix.attr }}'
|
||||||
|
|
||||||
|
codecov:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: "write"
|
||||||
|
contents: "read"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
|
||||||
|
- name: Run codecov
|
||||||
|
run: nix build .#checks.x86_64-linux.hello-llvm-cov
|
||||||
|
|
||||||
|
- name: Upload coverage reports to Codecov
|
||||||
|
uses: codecov/codecov-action@v4.0.1
|
||||||
|
with:
|
||||||
|
flags: unittests
|
||||||
|
name: codecov-hello
|
||||||
|
fail_ci_if_error: true
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
files: ./result
|
||||||
|
verbose: true
|
||||||
|
|
||||||
38
.github/workflows/docs.yaml
vendored
Normal file
38
.github/workflows/docs.yaml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: docs
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docs:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: "write"
|
||||||
|
contents: "read"
|
||||||
|
pages: "write"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
- uses: DeterminateSystems/flake-checker-action@main
|
||||||
|
|
||||||
|
- name: Generate docs
|
||||||
|
run: nix build .#checks.x86_64-linux.hello-docs
|
||||||
|
|
||||||
|
- name: Setup Pages
|
||||||
|
uses: actions/configure-pages@v5
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: result/share/doc
|
||||||
|
|
||||||
|
- name: Deploy to gh-pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
|
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/result
|
||||||
|
/target
|
||||||
|
.direnv
|
||||||
1148
Cargo.lock
generated
Normal file
1148
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
Cargo.toml
Normal file
15
Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "hyprmonitors"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum = "0.7"
|
||||||
|
hyprland = "0.4.0-beta.2"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
|
tower = "0.4"
|
||||||
|
tower-http = { version = "0.5", features = ["cors"] }
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
||||||
85
Makefile
Normal file
85
Makefile
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
.PHONY: build run dev clean install test check fmt clippy examples help
|
||||||
|
|
||||||
|
# Default target
|
||||||
|
all: build
|
||||||
|
|
||||||
|
# Build the project
|
||||||
|
build:
|
||||||
|
cargo build
|
||||||
|
|
||||||
|
# Build optimized release version
|
||||||
|
build-release:
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
|
# Run the server
|
||||||
|
run:
|
||||||
|
cargo run
|
||||||
|
|
||||||
|
# Run in development mode with logging
|
||||||
|
dev:
|
||||||
|
RUST_LOG=info cargo run
|
||||||
|
|
||||||
|
# Clean build artifacts
|
||||||
|
clean:
|
||||||
|
cargo clean
|
||||||
|
|
||||||
|
# Install dependencies and build
|
||||||
|
install:
|
||||||
|
cargo fetch
|
||||||
|
cargo build
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
test:
|
||||||
|
cargo test
|
||||||
|
|
||||||
|
# Check code without building
|
||||||
|
check:
|
||||||
|
cargo check
|
||||||
|
|
||||||
|
# Format code
|
||||||
|
fmt:
|
||||||
|
cargo fmt
|
||||||
|
|
||||||
|
# Run clippy linter
|
||||||
|
clippy:
|
||||||
|
cargo clippy -- -D warnings
|
||||||
|
|
||||||
|
# Run API examples (requires server to be running)
|
||||||
|
examples:
|
||||||
|
./examples.sh
|
||||||
|
|
||||||
|
# Start server in background and run examples
|
||||||
|
demo: build
|
||||||
|
@echo "Starting server in background..."
|
||||||
|
@cargo run & echo $$! > .server.pid
|
||||||
|
@sleep 3
|
||||||
|
@echo "Running examples..."
|
||||||
|
@./examples.sh || true
|
||||||
|
@echo "Stopping server..."
|
||||||
|
@if [ -f .server.pid ]; then kill `cat .server.pid` && rm .server.pid; fi
|
||||||
|
|
||||||
|
# Development setup
|
||||||
|
setup:
|
||||||
|
@echo "Setting up development environment..."
|
||||||
|
@rustup component add rustfmt clippy
|
||||||
|
@echo "Done! Run 'make run' to start the server."
|
||||||
|
|
||||||
|
# Show help
|
||||||
|
help:
|
||||||
|
@echo "Hyprmonitors Makefile"
|
||||||
|
@echo ""
|
||||||
|
@echo "Available targets:"
|
||||||
|
@echo " build - Build the project"
|
||||||
|
@echo " build-release - Build optimized release version"
|
||||||
|
@echo " run - Run the server"
|
||||||
|
@echo " dev - Run with debug logging"
|
||||||
|
@echo " clean - Clean build artifacts"
|
||||||
|
@echo " install - Install dependencies and build"
|
||||||
|
@echo " test - Run tests"
|
||||||
|
@echo " check - Check code without building"
|
||||||
|
@echo " fmt - Format code"
|
||||||
|
@echo " clippy - Run linter"
|
||||||
|
@echo " examples - Run API examples (server must be running)"
|
||||||
|
@echo " demo - Start server and run examples"
|
||||||
|
@echo " setup - Setup development environment"
|
||||||
|
@echo " help - Show this help"
|
||||||
189
README.md
Normal file
189
README.md
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
# Hyprmonitors
|
||||||
|
|
||||||
|
A Rust web server for controlling Hyprland desktop monitors via HTTP API using `hyprctl dispatch dpms` commands.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Turn all monitors on/off
|
||||||
|
- Control individual monitors by name
|
||||||
|
- Get current monitor status
|
||||||
|
- RESTful API with JSON responses
|
||||||
|
- CORS enabled for web applications
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Hyprland window manager
|
||||||
|
- Rust toolchain (1.70+)
|
||||||
|
- Running Hyprland session
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Clone or download this project
|
||||||
|
2. Build the project:
|
||||||
|
```bash
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Starting the Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
The server will start on `http://0.0.0.0:3000` by default.
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
#### Health Check
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Turn All Monitors On
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/monitors/on
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Turn All Monitors Off
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/monitors/off
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Turn Specific Monitor On
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/monitors/DP-1/on
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Turn Specific Monitor Off
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/monitors/DP-1/off
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get Monitor Status
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3000/monitors/status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Responses
|
||||||
|
|
||||||
|
#### Success Response
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Monitor DP-1 turned on",
|
||||||
|
"monitor": "DP-1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Status Response
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"monitors": {
|
||||||
|
"DP-1": "on",
|
||||||
|
"HDMI-A-1": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitor Names
|
||||||
|
|
||||||
|
To find your monitor names, you can use:
|
||||||
|
```bash
|
||||||
|
hyprctl monitors
|
||||||
|
```
|
||||||
|
|
||||||
|
Common monitor names include:
|
||||||
|
- `DP-1`, `DP-2` (DisplayPort)
|
||||||
|
- `HDMI-A-1`, `HDMI-A-2` (HDMI)
|
||||||
|
- `eDP-1` (Laptop screen)
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- `axum` - Web framework
|
||||||
|
- `hyprland` - Hyprland IPC client
|
||||||
|
- `tokio` - Async runtime
|
||||||
|
- `serde` - Serialization
|
||||||
|
- `tower-http` - HTTP middleware
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Debug build
|
||||||
|
cargo build
|
||||||
|
|
||||||
|
# Release build
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
|
# Run with logging
|
||||||
|
RUST_LOG=info cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
Test the endpoints manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test health
|
||||||
|
curl http://localhost:3000/health
|
||||||
|
|
||||||
|
# Test turning monitors off and on
|
||||||
|
curl -X POST http://localhost:3000/monitors/off
|
||||||
|
sleep 2
|
||||||
|
curl -X POST http://localhost:3000/monitors/on
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
curl http://localhost:3000/monitors/status
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The server binds to `0.0.0.0:3000` by default. To change the port, modify the `main.rs` file:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Systemd Service (Optional)
|
||||||
|
|
||||||
|
Create a systemd service to run the server automatically:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=Hyprland Monitor Control Server
|
||||||
|
After=graphical-session.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/path/to/hyprmonitors/target/release/hyprmonitors
|
||||||
|
Restart=always
|
||||||
|
User=%i
|
||||||
|
Environment=DISPLAY=:0
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is open source. Feel free to modify and distribute.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Server won't start
|
||||||
|
- Ensure you're running inside a Hyprland session
|
||||||
|
- Check that port 3000 is available
|
||||||
|
- Verify Rust toolchain is installed
|
||||||
|
|
||||||
|
### Monitor commands fail
|
||||||
|
- Confirm monitor names with `hyprctl monitors`
|
||||||
|
- Ensure Hyprland IPC is accessible
|
||||||
|
- Check server logs for error details
|
||||||
|
|
||||||
|
### Permission issues
|
||||||
|
- The server needs access to Hyprland's IPC socket
|
||||||
|
- Run the server as the same user running Hyprland
|
||||||
138
examples.sh
Executable file
138
examples.sh
Executable file
@@ -0,0 +1,138 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Hyprmonitors API Examples
|
||||||
|
# Make sure the server is running with: cargo run
|
||||||
|
|
||||||
|
BASE_URL="http://localhost:3000"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Function to print colored output
|
||||||
|
print_step() {
|
||||||
|
echo -e "${BLUE}$1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_success() {
|
||||||
|
echo -e "${GREEN}✓ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_warning() {
|
||||||
|
echo -e "${YELLOW}⚠ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}✗ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check if server is running
|
||||||
|
check_server() {
|
||||||
|
if ! curl -s "$BASE_URL/health" > /dev/null 2>&1; then
|
||||||
|
print_error "Server is not running at $BASE_URL"
|
||||||
|
print_warning "Please start the server with: cargo run"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
print_success "Server is running at $BASE_URL"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to make API call with error handling
|
||||||
|
api_call() {
|
||||||
|
local method="$1"
|
||||||
|
local endpoint="$2"
|
||||||
|
local description="$3"
|
||||||
|
|
||||||
|
print_step "$description"
|
||||||
|
|
||||||
|
local response
|
||||||
|
if [[ "$method" == "POST" ]]; then
|
||||||
|
response=$(curl -s -X POST "$BASE_URL$endpoint" 2>&1)
|
||||||
|
else
|
||||||
|
response=$(curl -s "$BASE_URL$endpoint" 2>&1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
local exit_code=$?
|
||||||
|
if [[ $exit_code -eq 0 ]]; then
|
||||||
|
echo "$response" | jq '.' 2>/dev/null || echo "$response"
|
||||||
|
print_success "Request completed"
|
||||||
|
else
|
||||||
|
print_error "Request failed: $response"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "=== Hyprmonitors API Examples ==="
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Check if required tools are available
|
||||||
|
if ! command -v curl &> /dev/null; then
|
||||||
|
print_error "curl is required but not installed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v jq &> /dev/null; then
|
||||||
|
print_warning "jq is not installed - output will not be formatted"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check server status
|
||||||
|
check_server
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
api_call "GET" "/health" "1. Health Check"
|
||||||
|
|
||||||
|
# Get monitor status
|
||||||
|
api_call "GET" "/monitors/status" "2. Current Monitor Status"
|
||||||
|
|
||||||
|
# Turn all monitors off
|
||||||
|
api_call "POST" "/monitors/off" "3. Turning all monitors OFF"
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Check status after turning off
|
||||||
|
api_call "GET" "/monitors/status" "4. Status after turning off"
|
||||||
|
|
||||||
|
# Turn all monitors on
|
||||||
|
api_call "POST" "/monitors/on" "5. Turning all monitors ON"
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Check status after turning on
|
||||||
|
api_call "GET" "/monitors/status" "6. Status after turning on"
|
||||||
|
|
||||||
|
# Get available monitors and test with first one
|
||||||
|
print_step "7. Testing specific monitor control"
|
||||||
|
monitor_status=$(curl -s "$BASE_URL/monitors/status" 2>/dev/null)
|
||||||
|
if [[ $? -eq 0 ]]; then
|
||||||
|
MONITOR_NAME=$(echo "$monitor_status" | jq -r '.monitors | keys[0]' 2>/dev/null)
|
||||||
|
if [[ "$MONITOR_NAME" != "null" && "$MONITOR_NAME" != "" ]]; then
|
||||||
|
print_success "Found monitor: $MONITOR_NAME"
|
||||||
|
api_call "POST" "/monitors/$MONITOR_NAME/off" " Turning $MONITOR_NAME off"
|
||||||
|
sleep 2
|
||||||
|
api_call "POST" "/monitors/$MONITOR_NAME/on" " Turning $MONITOR_NAME on"
|
||||||
|
else
|
||||||
|
print_warning "No monitors found for specific testing"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_warning "Could not retrieve monitor list for specific testing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Final status check
|
||||||
|
api_call "GET" "/monitors/status" "8. Final status check"
|
||||||
|
|
||||||
|
print_success "=== Examples completed ==="
|
||||||
|
echo
|
||||||
|
print_step "Available endpoints:"
|
||||||
|
echo " GET $BASE_URL/health"
|
||||||
|
echo " POST $BASE_URL/monitors/on"
|
||||||
|
echo " POST $BASE_URL/monitors/off"
|
||||||
|
echo " POST $BASE_URL/monitors/:monitor/on"
|
||||||
|
echo " POST $BASE_URL/monitors/:monitor/off"
|
||||||
|
echo " GET $BASE_URL/monitors/status"
|
||||||
|
echo
|
||||||
|
print_step "Tips:"
|
||||||
|
echo "• To find your monitor names, run: hyprctl monitors"
|
||||||
|
echo "• To start the server: cargo run"
|
||||||
|
echo "• To run with debug output: RUST_LOG=debug cargo run"
|
||||||
|
echo "• To change port: HYPRMONITORS_PORT=8080 cargo run"
|
||||||
96
flake.lock
generated
Normal file
96
flake.lock
generated
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1755027561,
|
||||||
|
"narHash": "sha256-IVft239Bc8p8Dtvf7UAACMG5P3ZV+3/aO28gXpGtMXI=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "005433b926e16227259a1843015b5b2b7f7d1fc3",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1744536153,
|
||||||
|
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"rust-overlay": "rust-overlay"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-overlay": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs_2"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1755225702,
|
||||||
|
"narHash": "sha256-i7Rgs943NqX0RgQW0/l1coi8eWBj3XhxVggMpjjzTsk=",
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"rev": "4abaeba6b176979be0da0195b9e4ce86bc501ae4",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
73
flake.nix
Normal file
73
flake.nix
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
description = "Hyprmonitors - Hyprland monitor control server";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = {
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
rust-overlay,
|
||||||
|
flake-utils,
|
||||||
|
}:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system: let
|
||||||
|
overlays = [(import rust-overlay)];
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system overlays;
|
||||||
|
};
|
||||||
|
|
||||||
|
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
|
||||||
|
extensions = ["rust-src" "rust-analyzer"];
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
rustToolchain
|
||||||
|
pkg-config
|
||||||
|
openssl
|
||||||
|
curl
|
||||||
|
jq
|
||||||
|
];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
echo "Hyprmonitors development environment"
|
||||||
|
echo "Rust version: $(rustc --version)"
|
||||||
|
echo ""
|
||||||
|
echo "Available commands:"
|
||||||
|
echo " cargo build - Build the project"
|
||||||
|
echo " cargo run - Run the server"
|
||||||
|
echo " cargo test - Run tests"
|
||||||
|
echo " ./examples.sh - Run API examples"
|
||||||
|
echo ""
|
||||||
|
'';
|
||||||
|
|
||||||
|
RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library";
|
||||||
|
};
|
||||||
|
|
||||||
|
packages.default = pkgs.rustPlatform.buildRustPackage {
|
||||||
|
pname = "hyprmonitors";
|
||||||
|
version = "0.1.0";
|
||||||
|
|
||||||
|
src = ./.;
|
||||||
|
|
||||||
|
cargoLock = {
|
||||||
|
lockFile = ./Cargo.lock;
|
||||||
|
};
|
||||||
|
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
openssl
|
||||||
|
pkg-config
|
||||||
|
];
|
||||||
|
|
||||||
|
meta = with pkgs.lib; {
|
||||||
|
description = "Hyprland monitor control server";
|
||||||
|
homepage = "https://github.com/your-username/hyprmonitors";
|
||||||
|
license = licenses.mit;
|
||||||
|
maintainers = [];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
49
hyprmonitors.service
Normal file
49
hyprmonitors.service
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Hyprland Monitor Control Server
|
||||||
|
Documentation=https://github.com/your-username/hyprmonitors
|
||||||
|
After=graphical-session.target
|
||||||
|
Wants=graphical-session.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/local/bin/hyprmonitors
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
User=%i
|
||||||
|
Group=users
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
Environment=RUST_LOG=info
|
||||||
|
Environment=HYPRMONITORS_HOST=127.0.0.1
|
||||||
|
Environment=HYPRMONITORS_PORT=3000
|
||||||
|
Environment=HYPRMONITORS_LOG_LEVEL=info
|
||||||
|
|
||||||
|
# Ensure Hyprland environment is available
|
||||||
|
Environment=XDG_RUNTIME_DIR=/run/user/1000
|
||||||
|
Environment=HYPRLAND_INSTANCE_SIGNATURE=%i
|
||||||
|
|
||||||
|
# Security settings
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectHome=read-only
|
||||||
|
ProtectSystem=strict
|
||||||
|
ReadWritePaths=/tmp
|
||||||
|
|
||||||
|
# Resource limits
|
||||||
|
LimitNOFILE=1024
|
||||||
|
MemoryMax=128M
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
|
||||||
|
# Installation Instructions:
|
||||||
|
# 1. Build the project: cargo build --release
|
||||||
|
# 2. Copy binary: sudo cp target/release/hyprmonitors /usr/local/bin/
|
||||||
|
# 3. Copy service file: sudo cp hyprmonitors.service /etc/systemd/system/hyprmonitors@.service
|
||||||
|
# 4. Enable for your user: systemctl --user enable hyprmonitors@$USER.service
|
||||||
|
# 5. Start the service: systemctl --user start hyprmonitors@$USER.service
|
||||||
|
# 6. Check status: systemctl --user status hyprmonitors@$USER.service
|
||||||
|
#
|
||||||
|
# For system-wide installation, modify paths and use:
|
||||||
|
# sudo systemctl enable hyprmonitors@username.service
|
||||||
|
# sudo systemctl start hyprmonitors@username.service
|
||||||
186
src/config.rs
Normal file
186
src/config.rs
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
pub struct Config {
|
||||||
|
pub server: ServerConfig,
|
||||||
|
pub hyprland: HyprlandConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ServerConfig {
|
||||||
|
pub host: String,
|
||||||
|
pub port: u16,
|
||||||
|
pub cors_enabled: bool,
|
||||||
|
pub log_level: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct HyprlandConfig {
|
||||||
|
pub timeout_ms: u64,
|
||||||
|
pub retry_attempts: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ServerConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
host: "0.0.0.0".to_string(),
|
||||||
|
port: 3000,
|
||||||
|
cors_enabled: true,
|
||||||
|
log_level: "info".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for HyprlandConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
timeout_ms: 5000,
|
||||||
|
retry_attempts: 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
/// Load configuration from environment variables with fallback to defaults
|
||||||
|
pub fn from_env() -> Self {
|
||||||
|
let mut config = Config::default();
|
||||||
|
|
||||||
|
// Server configuration
|
||||||
|
if let Ok(host) = env::var("HYPRMONITORS_HOST") {
|
||||||
|
config.server.host = host;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(port_str) = env::var("HYPRMONITORS_PORT") {
|
||||||
|
if let Ok(port) = port_str.parse::<u16>() {
|
||||||
|
config.server.port = port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(cors_str) = env::var("HYPRMONITORS_CORS_ENABLED") {
|
||||||
|
config.server.cors_enabled = cors_str.to_lowercase() == "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(log_level) = env::var("HYPRMONITORS_LOG_LEVEL") {
|
||||||
|
config.server.log_level = log_level;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hyprland configuration
|
||||||
|
if let Ok(timeout_str) = env::var("HYPRMONITORS_TIMEOUT_MS") {
|
||||||
|
if let Ok(timeout) = timeout_str.parse::<u64>() {
|
||||||
|
config.hyprland.timeout_ms = timeout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(retry_str) = env::var("HYPRMONITORS_RETRY_ATTEMPTS") {
|
||||||
|
if let Ok(retry) = retry_str.parse::<u32>() {
|
||||||
|
config.hyprland.retry_attempts = retry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the server bind address
|
||||||
|
pub fn bind_address(&self) -> String {
|
||||||
|
format!("{}:{}", self.server.host, self.server.port)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the full server URL for logging
|
||||||
|
pub fn server_url(&self) -> String {
|
||||||
|
format!("http://{}:{}", self.server.host, self.server.port)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print configuration summary
|
||||||
|
pub fn print_summary(&self) {
|
||||||
|
tracing::info!("Configuration:");
|
||||||
|
tracing::info!(" Server: {}", self.server_url());
|
||||||
|
tracing::info!(" CORS: {}", self.server.cors_enabled);
|
||||||
|
tracing::info!(" Log Level: {}", self.server.log_level);
|
||||||
|
tracing::info!(" Hyprland Timeout: {}ms", self.hyprland.timeout_ms);
|
||||||
|
tracing::info!(" Retry Attempts: {}", self.hyprland.retry_attempts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_default_config() {
|
||||||
|
let config = Config::default();
|
||||||
|
assert_eq!(config.server.host, "0.0.0.0");
|
||||||
|
assert_eq!(config.server.port, 3000);
|
||||||
|
assert_eq!(config.server.cors_enabled, true);
|
||||||
|
assert_eq!(config.server.log_level, "info");
|
||||||
|
assert_eq!(config.hyprland.timeout_ms, 5000);
|
||||||
|
assert_eq!(config.hyprland.retry_attempts, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bind_address() {
|
||||||
|
let config = Config::default();
|
||||||
|
assert_eq!(config.bind_address(), "0.0.0.0:3000");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_server_url() {
|
||||||
|
let config = Config::default();
|
||||||
|
assert_eq!(config.server_url(), "http://0.0.0.0:3000");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_env_config() {
|
||||||
|
// Clean up any existing env vars first
|
||||||
|
env::remove_var("HYPRMONITORS_HOST");
|
||||||
|
env::remove_var("HYPRMONITORS_PORT");
|
||||||
|
env::remove_var("HYPRMONITORS_CORS_ENABLED");
|
||||||
|
env::remove_var("HYPRMONITORS_LOG_LEVEL");
|
||||||
|
env::remove_var("HYPRMONITORS_TIMEOUT_MS");
|
||||||
|
env::remove_var("HYPRMONITORS_RETRY_ATTEMPTS");
|
||||||
|
|
||||||
|
env::set_var("HYPRMONITORS_HOST", "127.0.0.1");
|
||||||
|
env::set_var("HYPRMONITORS_PORT", "8080");
|
||||||
|
env::set_var("HYPRMONITORS_CORS_ENABLED", "false");
|
||||||
|
env::set_var("HYPRMONITORS_LOG_LEVEL", "debug");
|
||||||
|
env::set_var("HYPRMONITORS_TIMEOUT_MS", "10000");
|
||||||
|
env::set_var("HYPRMONITORS_RETRY_ATTEMPTS", "5");
|
||||||
|
|
||||||
|
let config = Config::from_env();
|
||||||
|
|
||||||
|
assert_eq!(config.server.host, "127.0.0.1");
|
||||||
|
assert_eq!(config.server.port, 8080);
|
||||||
|
assert_eq!(config.server.cors_enabled, false);
|
||||||
|
assert_eq!(config.server.log_level, "debug");
|
||||||
|
assert_eq!(config.hyprland.timeout_ms, 10000);
|
||||||
|
assert_eq!(config.hyprland.retry_attempts, 5);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
env::remove_var("HYPRMONITORS_HOST");
|
||||||
|
env::remove_var("HYPRMONITORS_PORT");
|
||||||
|
env::remove_var("HYPRMONITORS_CORS_ENABLED");
|
||||||
|
env::remove_var("HYPRMONITORS_LOG_LEVEL");
|
||||||
|
env::remove_var("HYPRMONITORS_TIMEOUT_MS");
|
||||||
|
env::remove_var("HYPRMONITORS_RETRY_ATTEMPTS");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_env_values() {
|
||||||
|
// Clean up any existing env vars first
|
||||||
|
env::remove_var("HYPRMONITORS_PORT");
|
||||||
|
env::remove_var("HYPRMONITORS_TIMEOUT_MS");
|
||||||
|
|
||||||
|
env::set_var("HYPRMONITORS_PORT", "invalid");
|
||||||
|
env::set_var("HYPRMONITORS_TIMEOUT_MS", "not_a_number");
|
||||||
|
|
||||||
|
let config = Config::from_env();
|
||||||
|
|
||||||
|
// Should fall back to defaults for invalid values
|
||||||
|
assert_eq!(config.server.port, 3000);
|
||||||
|
assert_eq!(config.hyprland.timeout_ms, 5000);
|
||||||
|
|
||||||
|
env::remove_var("HYPRMONITORS_PORT");
|
||||||
|
env::remove_var("HYPRMONITORS_TIMEOUT_MS");
|
||||||
|
}
|
||||||
|
}
|
||||||
301
src/lib.rs
Normal file
301
src/lib.rs
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
//! Hyprmonitors Library
|
||||||
|
//!
|
||||||
|
//! A Rust library for controlling Hyprland desktop monitors via HTTP API.
|
||||||
|
|
||||||
|
pub mod config;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
extract::Path,
|
||||||
|
http::StatusCode,
|
||||||
|
response::Json,
|
||||||
|
routing::{get, post},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use hyprland::dispatch::{Dispatch, DispatchType};
|
||||||
|
use hyprland::shared::HyprData;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use tower_http::cors::CorsLayer;
|
||||||
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
|
pub use config::Config;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct MonitorResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub message: String,
|
||||||
|
pub monitor: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct MonitorRequest {
|
||||||
|
pub monitor: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct StatusResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub monitors: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create the main application router with all endpoints
|
||||||
|
pub fn create_app(config: &Config) -> Router {
|
||||||
|
let mut app = Router::new()
|
||||||
|
.route("/", get(health_check))
|
||||||
|
.route("/health", get(health_check))
|
||||||
|
.route("/monitors/on", post(turn_all_monitors_on))
|
||||||
|
.route("/monitors/off", post(turn_all_monitors_off))
|
||||||
|
.route("/monitors/:monitor/on", post(turn_monitor_on))
|
||||||
|
.route("/monitors/:monitor/off", post(turn_monitor_off))
|
||||||
|
.route("/monitors/status", get(get_monitor_status));
|
||||||
|
|
||||||
|
// Add CORS if enabled
|
||||||
|
if config.server.cors_enabled {
|
||||||
|
app = app.layer(CorsLayer::permissive());
|
||||||
|
}
|
||||||
|
|
||||||
|
app
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify connection to Hyprland
|
||||||
|
pub async fn verify_hyprland_connection() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
match hyprland::data::Monitors::get() {
|
||||||
|
Ok(monitors) => {
|
||||||
|
let monitor_vec: Vec<_> = monitors.into_iter().collect();
|
||||||
|
info!(
|
||||||
|
"Successfully connected to Hyprland, found {} monitors",
|
||||||
|
monitor_vec.len()
|
||||||
|
);
|
||||||
|
for monitor in monitor_vec {
|
||||||
|
info!(
|
||||||
|
" Monitor: {} ({}x{}) - DPMS: {}",
|
||||||
|
monitor.name,
|
||||||
|
monitor.width,
|
||||||
|
monitor.height,
|
||||||
|
if monitor.dpms_status { "on" } else { "off" }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to connect to Hyprland: {}", e);
|
||||||
|
Err(e.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Health check endpoint
|
||||||
|
pub async fn health_check() -> Json<MonitorResponse> {
|
||||||
|
Json(MonitorResponse {
|
||||||
|
success: true,
|
||||||
|
message: "Hyprland Monitor Control Server is running".to_string(),
|
||||||
|
monitor: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turn all monitors on
|
||||||
|
pub async fn turn_all_monitors_on() -> Result<Json<MonitorResponse>, StatusCode> {
|
||||||
|
match execute_dpms_command("on", None).await {
|
||||||
|
Ok(_) => {
|
||||||
|
info!("All monitors turned on");
|
||||||
|
Ok(Json(MonitorResponse {
|
||||||
|
success: true,
|
||||||
|
message: "All monitors turned on".to_string(),
|
||||||
|
monitor: None,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to turn on all monitors: {}", e);
|
||||||
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turn all monitors off
|
||||||
|
pub async fn turn_all_monitors_off() -> Result<Json<MonitorResponse>, StatusCode> {
|
||||||
|
match execute_dpms_command("off", None).await {
|
||||||
|
Ok(_) => {
|
||||||
|
info!("All monitors turned off");
|
||||||
|
Ok(Json(MonitorResponse {
|
||||||
|
success: true,
|
||||||
|
message: "All monitors turned off".to_string(),
|
||||||
|
monitor: None,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to turn off all monitors: {}", e);
|
||||||
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turn specific monitor on
|
||||||
|
pub async fn turn_monitor_on(
|
||||||
|
Path(monitor): Path<String>,
|
||||||
|
) -> Result<Json<MonitorResponse>, StatusCode> {
|
||||||
|
match execute_dpms_command("on", Some(&monitor)).await {
|
||||||
|
Ok(_) => {
|
||||||
|
info!("Monitor {} turned on", monitor);
|
||||||
|
Ok(Json(MonitorResponse {
|
||||||
|
success: true,
|
||||||
|
message: format!("Monitor {} turned on", monitor),
|
||||||
|
monitor: Some(monitor),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to turn on monitor {}: {}", monitor, e);
|
||||||
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turn specific monitor off
|
||||||
|
pub async fn turn_monitor_off(
|
||||||
|
Path(monitor): Path<String>,
|
||||||
|
) -> Result<Json<MonitorResponse>, StatusCode> {
|
||||||
|
match execute_dpms_command("off", Some(&monitor)).await {
|
||||||
|
Ok(_) => {
|
||||||
|
info!("Monitor {} turned off", monitor);
|
||||||
|
Ok(Json(MonitorResponse {
|
||||||
|
success: true,
|
||||||
|
message: format!("Monitor {} turned off", monitor),
|
||||||
|
monitor: Some(monitor),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to turn off monitor {}: {}", monitor, e);
|
||||||
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get monitor status
|
||||||
|
pub async fn get_monitor_status() -> Result<Json<StatusResponse>, StatusCode> {
|
||||||
|
match hyprland::data::Monitors::get() {
|
||||||
|
Ok(monitors) => {
|
||||||
|
let mut monitor_map = HashMap::new();
|
||||||
|
for monitor in monitors.into_iter() {
|
||||||
|
let status = if monitor.dpms_status { "on" } else { "off" };
|
||||||
|
monitor_map.insert(monitor.name, status.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Retrieved status for {} monitors", monitor_map.len());
|
||||||
|
Ok(Json(StatusResponse {
|
||||||
|
success: true,
|
||||||
|
monitors: monitor_map,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to get monitor status: {}", e);
|
||||||
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute DPMS command via Hyprland dispatch
|
||||||
|
pub async fn execute_dpms_command(
|
||||||
|
action: &str,
|
||||||
|
monitor: Option<&str>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let command = match monitor {
|
||||||
|
Some(monitor_name) => format!("dpms {} {}", action, monitor_name),
|
||||||
|
None => format!("dpms {}", action),
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("Executing hyprctl dispatch: {}", command);
|
||||||
|
|
||||||
|
// Validate monitor exists if specified
|
||||||
|
if let Some(monitor_name) = monitor {
|
||||||
|
if !monitor_exists(monitor_name).await? {
|
||||||
|
warn!("Monitor '{}' not found, proceeding anyway", monitor_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the command for hyprland dispatch
|
||||||
|
let dispatch_type = match action {
|
||||||
|
"on" => {
|
||||||
|
if let Some(monitor_name) = monitor {
|
||||||
|
DispatchType::Custom("dpms", &format!("on {}", monitor_name))
|
||||||
|
} else {
|
||||||
|
DispatchType::Custom("dpms", "on")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"off" => {
|
||||||
|
if let Some(monitor_name) = monitor {
|
||||||
|
DispatchType::Custom("dpms", &format!("off {}", monitor_name))
|
||||||
|
} else {
|
||||||
|
DispatchType::Custom("dpms", "off")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => return Err("Invalid action".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
Dispatch::call(dispatch_type)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a monitor exists
|
||||||
|
pub async fn monitor_exists(monitor_name: &str) -> Result<bool, Box<dyn std::error::Error>> {
|
||||||
|
let monitors = hyprland::data::Monitors::get()?;
|
||||||
|
Ok(monitors.into_iter().any(|m| m.name == monitor_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get list of available monitors
|
||||||
|
pub async fn get_available_monitors() -> Result<Vec<String>, Box<dyn std::error::Error>> {
|
||||||
|
let monitors = hyprland::data::Monitors::get()?;
|
||||||
|
Ok(monitors.into_iter().map(|m| m.name).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_monitor_response_creation() {
|
||||||
|
let response = MonitorResponse {
|
||||||
|
success: true,
|
||||||
|
message: "Test message".to_string(),
|
||||||
|
monitor: Some("DP-1".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(response.success);
|
||||||
|
assert_eq!(response.message, "Test message");
|
||||||
|
assert_eq!(response.monitor, Some("DP-1".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_status_response_creation() {
|
||||||
|
let mut monitors = HashMap::new();
|
||||||
|
monitors.insert("DP-1".to_string(), "on".to_string());
|
||||||
|
monitors.insert("HDMI-A-1".to_string(), "off".to_string());
|
||||||
|
|
||||||
|
let response = StatusResponse {
|
||||||
|
success: true,
|
||||||
|
monitors,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(response.success);
|
||||||
|
assert_eq!(response.monitors.len(), 2);
|
||||||
|
assert_eq!(response.monitors.get("DP-1"), Some(&"on".to_string()));
|
||||||
|
assert_eq!(response.monitors.get("HDMI-A-1"), Some(&"off".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_health_check() {
|
||||||
|
let response = health_check().await;
|
||||||
|
assert!(response.0.success);
|
||||||
|
assert_eq!(
|
||||||
|
response.0.message,
|
||||||
|
"Hyprland Monitor Control Server is running"
|
||||||
|
);
|
||||||
|
assert_eq!(response.0.monitor, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_app() {
|
||||||
|
let config = Config::default();
|
||||||
|
let _app = create_app(&config);
|
||||||
|
// This test just ensures the app can be created without panicking
|
||||||
|
assert!(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/main.rs
Normal file
52
src/main.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
use hyprmonitors::{create_app, verify_hyprland_connection, Config};
|
||||||
|
use tracing::{error, info};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Load configuration
|
||||||
|
let config = Config::from_env();
|
||||||
|
|
||||||
|
// Initialize tracing with configured log level
|
||||||
|
let log_level = config
|
||||||
|
.server
|
||||||
|
.log_level
|
||||||
|
.parse()
|
||||||
|
.unwrap_or(tracing::Level::INFO);
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_target(false)
|
||||||
|
.with_max_level(log_level)
|
||||||
|
.compact()
|
||||||
|
.init();
|
||||||
|
|
||||||
|
// Print configuration summary
|
||||||
|
config.print_summary();
|
||||||
|
|
||||||
|
// Verify Hyprland connection
|
||||||
|
if let Err(e) = verify_hyprland_connection().await {
|
||||||
|
error!("Failed to connect to Hyprland: {}", e);
|
||||||
|
error!("Make sure you're running this inside a Hyprland session");
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the application
|
||||||
|
let app = create_app(&config);
|
||||||
|
|
||||||
|
// Run the server
|
||||||
|
let bind_addr = config.bind_address();
|
||||||
|
let listener = tokio::net::TcpListener::bind(&bind_addr).await?;
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Hyprland Monitor Control Server running on {}",
|
||||||
|
config.server_url()
|
||||||
|
);
|
||||||
|
info!("Available endpoints:");
|
||||||
|
info!(" GET /health - Health check");
|
||||||
|
info!(" POST /monitors/on - Turn all monitors on");
|
||||||
|
info!(" POST /monitors/off - Turn all monitors off");
|
||||||
|
info!(" POST /monitors/:monitor/on - Turn specific monitor on");
|
||||||
|
info!(" POST /monitors/:monitor/off - Turn specific monitor off");
|
||||||
|
info!(" GET /monitors/status - Get monitor status");
|
||||||
|
|
||||||
|
axum::serve(listener, app).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user