From f0dce3f2332d1909797acb695744944c3b9bdc71 Mon Sep 17 00:00:00 2001 From: uttarayan21 Date: Fri, 15 Aug 2025 16:28:28 +0530 Subject: [PATCH] feat: initail commit --- .env.example | 17 + .envrc | 1 + .github/workflows/build.yaml | 62 ++ .github/workflows/docs.yaml | 38 ++ .gitignore | 3 + Cargo.lock | 1148 ++++++++++++++++++++++++++++++++++ Cargo.toml | 15 + Makefile | 85 +++ README.md | 189 ++++++ examples.sh | 138 ++++ flake.lock | 96 +++ flake.nix | 73 +++ hyprmonitors.service | 49 ++ src/config.rs | 186 ++++++ src/lib.rs | 301 +++++++++ src/main.rs | 52 ++ 16 files changed, 2453 insertions(+) create mode 100644 .env.example create mode 100644 .envrc create mode 100644 .github/workflows/build.yaml create mode 100644 .github/workflows/docs.yaml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Makefile create mode 100644 README.md create mode 100755 examples.sh create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 hyprmonitors.service create mode 100644 src/config.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..576defb --- /dev/null +++ b/.env.example @@ -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 diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..d8894ee --- /dev/null +++ b/.github/workflows/build.yaml @@ -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 + diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml new file mode 100644 index 0000000..c7048c4 --- /dev/null +++ b/.github/workflows/docs.yaml @@ -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 + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6210698 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/result +/target +.direnv diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..2d37bb5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1148 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower 0.5.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyprland" +version = "0.4.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc9c1413b6f0fd10b2e4463479490e30b2497ae4449f044da16053f5f2cb03b8" +dependencies = [ + "ahash", + "async-stream", + "derive_more", + "either", + "futures-lite", + "hyprland-macros", + "num-traits", + "once_cell", + "paste", + "phf", + "serde", + "serde_json", + "serde_repr", + "tokio", +] + +[[package]] +name = "hyprland-macros" +version = "0.4.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e3cbed6e560408051175d29a9ed6ad1e64a7ff443836addf797b0479f58983" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "hyprmonitors" +version = "0.1.0" +dependencies = [ + "axum", + "hyprland", + "serde", + "serde_json", + "tokio", + "tower 0.4.13", + "tower-http", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "proc-macro2" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.142" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "syn" +version = "2.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags", + "bytes", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3dc1442 --- /dev/null +++ b/Cargo.toml @@ -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"] } diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..764f410 --- /dev/null +++ b/Makefile @@ -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" diff --git a/README.md b/README.md new file mode 100644 index 0000000..f99396a --- /dev/null +++ b/README.md @@ -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 \ No newline at end of file diff --git a/examples.sh b/examples.sh new file mode 100755 index 0000000..1a209ee --- /dev/null +++ b/examples.sh @@ -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" diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..ff1a7c7 --- /dev/null +++ b/flake.lock @@ -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 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..65f7def --- /dev/null +++ b/flake.nix @@ -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 = []; + }; + }; + }); +} diff --git a/hyprmonitors.service b/hyprmonitors.service new file mode 100644 index 0000000..00960f0 --- /dev/null +++ b/hyprmonitors.service @@ -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 diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..d6b9eae --- /dev/null +++ b/src/config.rs @@ -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::() { + 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::() { + config.hyprland.timeout_ms = timeout; + } + } + + if let Ok(retry_str) = env::var("HYPRMONITORS_RETRY_ATTEMPTS") { + if let Ok(retry) = retry_str.parse::() { + 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"); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..e587de5 --- /dev/null +++ b/src/lib.rs @@ -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, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct MonitorRequest { + pub monitor: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct StatusResponse { + pub success: bool, + pub monitors: HashMap, +} + +/// 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> { + 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 { + 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, 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, 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, +) -> Result, 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, +) -> Result, 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, 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> { + 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> { + 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, Box> { + 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); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..52c0169 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,52 @@ +use hyprmonitors::{create_app, verify_hyprland_connection, Config}; +use tracing::{error, info}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // 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(()) +}