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