Compare commits

...

12 Commits

Author SHA1 Message Date
e8221faeb1 Convert to Babashka script with modern idle detection
Replace heavyweight JVM Clojure with Babashka for instant startup and
simplicity. The script now uses mosquitto_pub CLI tool instead of the
Java MQTT client library.

Key improvements:
- Use systemd-logind for idle detection (works with X11 and Wayland)
- Fallback to xprintidle for legacy X11-only systems
- Replace Paho MQTT client with mosquitto_pub CLI
- Eliminate all external dependencies (use Babashka built-ins)
- Add shebang for direct execution
- Fix compatibility with NixOS 25.11 and modern Wayland systems

This resolves the 'screen saver extension not supported' error by using
loginctl to query systemd-logind instead of relying on X11 extensions.
2026-02-15 23:42:53 -08:00
3c17eab1ed Update deps 2026-02-09 15:38:24 -08:00
3755693de6 Update deps-lock.json 2026-02-09 15:19:58 -08:00
5937d793e2 Updated deps 2026-02-02 17:47:24 -08:00
0fe6aa147a Migrate test runner from cognitect.test-runner to eftest
- Replace cognitect.test-runner with eftest 0.6.0
- Update Clojure and core dependencies to latest versions
- Update fudo-clojure to latest SHA
2026-02-02 15:59:45 -08:00
5f366ca174 Update library deps to use preppedSrc and regenerate deps-lock.json 2026-02-02 11:08:06 -08:00
6725174e36 Update helpers to version with legacyPackages 2026-02-02 10:17:02 -08:00
d6f59437c2 Update to use helpers.legacyPackages instead of helpers.packages
Builder functions have been moved to legacyPackages in fudo-nix-helpers
to satisfy nix flake check requirements.
2026-02-02 08:58:36 -08:00
ecbd4942ad update dependencies 2024-07-12 19:34:51 -07:00
5c9c7b8359 switch to github 2024-06-25 19:44:18 -07:00
f079b882da Update deps with helpers 2024-04-21 12:11:28 -07:00
114dc5d9de updated flake.lock 2024-03-24 09:19:47 -07:00
6 changed files with 1862 additions and 162 deletions

1
.gitignore vendored
View File

@@ -7,3 +7,4 @@ tmp/
.nrepl-port
target/
result
.clj-kondo/

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +1,29 @@
{
:paths ["src"]
:deps {
org.clojure/clojure { :mvn/version "1.11.1" }
org.clojure/core.async { :mvn/version "1.5.648" }
org.clojure/tools.cli { :mvn/version "1.0.206" }
org.fudo/fudo-clojure {
:git/url "https://git.fudo.org/fudo-public/fudo-clojure.git"
:sha "c6a1ebef2e5b64d432a46ac48639c674e62b7cee"
}
org.eclipse.paho/org.eclipse.paho.client.mqttv3 { :mvn/version "1.2.5" }
camel-snake-kebab/camel-snake-kebab { :mvn/version "0.4.2" }
;; All dependencies are now built into Babashka:
;; - babashka.process (for shell commands)
;; - clojure.core.async (for concurrency)
;; - clojure.tools.cli (for CLI parsing)
;; - cheshire.core (for JSON)
;;
;; This script no longer requires any external dependencies!
}
:aliases {
;; For running with regular Clojure (if needed for development):
:jvm {
:extra-deps {
org.clojure/clojure { :mvn/version "1.12.3" }
org.clojure/core.async { :mvn/version "1.8.741" }
org.clojure/tools.cli { :mvn/version "1.3.250" }
}
}
:test {
:extra-paths ["test"]
:extra-deps {
io.github.cognitect-labs/test-runner
{
:git/url "https://github.com/cognitect-labs/test-runner.git"
:sha "dfb30dd6605cb6c0efc275e1df1736f6e90d4d73"
}
eftest/eftest {:mvn/version "0.6.0"}
}
:main-opts ["-m" "cognitect.test-runner"]
:exec-fn cognitect.test-runner.api/test
:main-opts ["-e" "(require '[eftest.runner :refer [find-tests run-tests]]) (run-tests (find-tests \"test\"))"]
}
:build { :default-ns build }
}
}

632
flake.lock generated
View File

@@ -3,39 +3,109 @@
"clj-nix": {
"inputs": {
"devshell": "devshell",
"flake-utils": "flake-utils_2",
"nix-fetcher-data": "nix-fetcher-data",
"nixpkgs": [
"fudo-clojure",
"helpers",
"nixpkgs"
]
},
"locked": {
"lastModified": 1663870497,
"narHash": "sha256-gnoyYWvZl64WBqR3tf9bKHAznEtBCHmwx7taHghH9Lw=",
"lastModified": 1732066389,
"narHash": "sha256-6z9KTXwDQN14vljs5USgb4Pr8BYk46Q2yhkcdNx9QEE=",
"owner": "jlesquembre",
"repo": "clj-nix",
"rev": "23d9daacc80e634df078c4c6e34d592e1593d84c",
"rev": "95f26552686259f64685569e6966cdefebb0a6a9",
"type": "github"
},
"original": {
"owner": "jlesquembre",
"ref": "0.4.0",
"repo": "clj-nix",
"type": "github"
}
},
"clj-nix_2": {
"inputs": {
"devshell": "devshell_2",
"nix-fetcher-data": "nix-fetcher-data_2",
"nixpkgs": [
"helpers",
"nixpkgs"
]
},
"locked": {
"lastModified": 1732066389,
"narHash": "sha256-6z9KTXwDQN14vljs5USgb4Pr8BYk46Q2yhkcdNx9QEE=",
"owner": "jlesquembre",
"repo": "clj-nix",
"rev": "95f26552686259f64685569e6966cdefebb0a6a9",
"type": "github"
},
"original": {
"owner": "jlesquembre",
"ref": "0.4.0",
"repo": "clj-nix",
"type": "github"
}
},
"clj2nix": {
"inputs": {
"flake-compat": "flake-compat",
"gitignore": "gitignore",
"nixpkgs": "nixpkgs",
"utils": "utils"
},
"locked": {
"lastModified": 1727456190,
"narHash": "sha256-aZvIrzSQQ6ATmAHeitqchAD2DTLcWV+PU8qs80fq3lA=",
"owner": "hlolli",
"repo": "clj2nix",
"rev": "4a968eca5d368b5c818081333a64a3eb93e50d24",
"type": "github"
},
"original": {
"owner": "hlolli",
"repo": "clj2nix",
"type": "github"
}
},
"clj2nix_2": {
"inputs": {
"flake-compat": "flake-compat_2",
"gitignore": "gitignore_2",
"nixpkgs": "nixpkgs_4",
"utils": "utils_4"
},
"locked": {
"lastModified": 1727456190,
"narHash": "sha256-aZvIrzSQQ6ATmAHeitqchAD2DTLcWV+PU8qs80fq3lA=",
"owner": "hlolli",
"repo": "clj2nix",
"rev": "4a968eca5d368b5c818081333a64a3eb93e50d24",
"type": "github"
},
"original": {
"owner": "hlolli",
"repo": "clj2nix",
"type": "github"
}
},
"devshell": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"fudo-clojure",
"helpers",
"clj-nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1658746384,
"narHash": "sha256-CCJcoMOcXyZFrV1ag4XMTpAPjLWb4Anbv+ktXFI1ry0=",
"lastModified": 1728330715,
"narHash": "sha256-xRJ2nPOXb//u1jaBnDP56M7v5ldavjbtR6lfGqSvcKg=",
"owner": "numtide",
"repo": "devshell",
"rev": "0ffc7937bb5e8141af03d462b468bd071eb18e1b",
"rev": "dd6b80932022cea34a019e2bb32f6fa9e494dfef",
"type": "github"
},
"original": {
@@ -44,56 +114,473 @@
"type": "github"
}
},
"flake-utils": {
"devshell_2": {
"inputs": {
"nixpkgs": [
"helpers",
"clj-nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1642700792,
"narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=",
"lastModified": 1728330715,
"narHash": "sha256-xRJ2nPOXb//u1jaBnDP56M7v5ldavjbtR6lfGqSvcKg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "846b2ae0fc4cc943637d3d1def4454213e203cba",
"repo": "devshell",
"rev": "dd6b80932022cea34a019e2bb32f6fa9e494dfef",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"repo": "devshell",
"type": "github"
}
},
"flake-utils_2": {
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1656928814,
"narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249",
"lastModified": 1668681692,
"narHash": "sha256-Ht91NGdewz8IQLtWZ9LCeNXMSXHUss+9COoqu6JLmXU=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "009399224d5e398d03b22badca40a37ac85412a1",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-compat_2": {
"flake": false,
"locked": {
"lastModified": 1668681692,
"narHash": "sha256-Ht91NGdewz8IQLtWZ9LCeNXMSXHUss+9COoqu6JLmXU=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "009399224d5e398d03b22badca40a37ac85412a1",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1719745305,
"narHash": "sha256-xwgjVUpqSviudEkpQnioeez1Uo2wzrsMaJKJClh+Bls=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "c3c5ecc05edc7dafba779c6c1a61cd08ac6583e9",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_2": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib_2"
},
"locked": {
"lastModified": 1719745305,
"narHash": "sha256-xwgjVUpqSviudEkpQnioeez1Uo2wzrsMaJKJClh+Bls=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "c3c5ecc05edc7dafba779c6c1a61cd08ac6583e9",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"fudo-clojure": {
"inputs": {
"helpers": "helpers",
"nixpkgs": "nixpkgs_3",
"utils": "utils_3"
},
"locked": {
"lastModified": 1770077493,
"narHash": "sha256-HgmKxXAcQBZ1l/olVlQWWyble8BdJdL7QgP7sJ2pXy4=",
"owner": "fudoniten",
"repo": "fudo-clojure",
"rev": "107e56fa3222e9472892b0f3cad358223a91a257",
"type": "github"
},
"original": {
"owner": "fudoniten",
"repo": "fudo-clojure",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"fudo-clojure",
"helpers",
"clj2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1660459072,
"narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "a20de23b925fd8264fd7fad6454652e142fd7f73",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"gitignore_2": {
"inputs": {
"nixpkgs": [
"helpers",
"clj2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1660459072,
"narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "a20de23b925fd8264fd7fad6454652e142fd7f73",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"helpers": {
"inputs": {
"clj-nix": "clj-nix",
"clj2nix": "clj2nix",
"nix2container": "nix2container",
"nixpkgs": [
"fudo-clojure",
"nixpkgs"
],
"utils": "utils_2"
},
"locked": {
"lastModified": 1770058348,
"narHash": "sha256-jqtOJhEOUCjJ8RoLkfejOlUyzSAHr6YHYwGXoUSnW5c=",
"owner": "fudoniten",
"repo": "fudo-nix-helpers",
"rev": "89ad3f7ca2cff121475794bde7c6e29e0e5169d1",
"type": "github"
},
"original": {
"owner": "fudoniten",
"repo": "fudo-nix-helpers",
"type": "github"
}
},
"helpers_2": {
"inputs": {
"clj-nix": "clj-nix_2",
"clj2nix": "clj2nix_2",
"nix2container": "nix2container_2",
"nixpkgs": [
"nixpkgs"
],
"utils": "utils_5"
},
"locked": {
"lastModified": 1770058348,
"narHash": "sha256-jqtOJhEOUCjJ8RoLkfejOlUyzSAHr6YHYwGXoUSnW5c=",
"owner": "fudoniten",
"repo": "fudo-nix-helpers",
"rev": "89ad3f7ca2cff121475794bde7c6e29e0e5169d1",
"type": "github"
},
"original": {
"owner": "fudoniten",
"repo": "fudo-nix-helpers",
"type": "github"
}
},
"nix-fetcher-data": {
"inputs": {
"flake-parts": "flake-parts",
"nixpkgs": [
"fudo-clojure",
"helpers",
"clj-nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1728229178,
"narHash": "sha256-p5Fx880uBYstIsbaDYN7sECJT11oHxZQKtHgMAVblWA=",
"owner": "jlesquembre",
"repo": "nix-fetcher-data",
"rev": "f3a73c34d28db49ef90fd7872a142bfe93120e55",
"type": "github"
},
"original": {
"owner": "jlesquembre",
"repo": "nix-fetcher-data",
"type": "github"
}
},
"nix-fetcher-data_2": {
"inputs": {
"flake-parts": "flake-parts_2",
"nixpkgs": [
"helpers",
"clj-nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1728229178,
"narHash": "sha256-p5Fx880uBYstIsbaDYN7sECJT11oHxZQKtHgMAVblWA=",
"owner": "jlesquembre",
"repo": "nix-fetcher-data",
"rev": "f3a73c34d28db49ef90fd7872a142bfe93120e55",
"type": "github"
},
"original": {
"owner": "jlesquembre",
"repo": "nix-fetcher-data",
"type": "github"
}
},
"nix2container": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1767430085,
"narHash": "sha256-SiXJ6xv4pS2MDUqfj0/mmG746cGeJrMQGmoFgHLS25Y=",
"owner": "nlewo",
"repo": "nix2container",
"rev": "66f4b8a47e92aa744ec43acbb5e9185078983909",
"type": "github"
},
"original": {
"owner": "nlewo",
"repo": "nix2container",
"type": "github"
}
},
"nix2container_2": {
"inputs": {
"nixpkgs": "nixpkgs_5"
},
"locked": {
"lastModified": 1767430085,
"narHash": "sha256-SiXJ6xv4pS2MDUqfj0/mmG746cGeJrMQGmoFgHLS25Y=",
"owner": "nlewo",
"repo": "nix2container",
"rev": "66f4b8a47e92aa744ec43acbb5e9185078983909",
"type": "github"
},
"original": {
"owner": "nlewo",
"repo": "nix2container",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1672580127,
"narHash": "sha256-3lW3xZslREhJogoOkjeZtlBtvFMyxHku7I/9IVehhT8=",
"lastModified": 1673785507,
"narHash": "sha256-EPUT8yVdvJhhjhbgnFWXXd4IUPKSOmww2+z4AmOdyPI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0874168639713f547c05947c76124f78441ea46c",
"rev": "d06d765eeac716d8f1ca80f0935fd6fc951816ad",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1717284937,
"narHash": "sha256-lIbdfCsf8LMFloheeE6N31+BMIeixqyQWbSr2vk79EQ=",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz"
}
},
"nixpkgs-lib_2": {
"locked": {
"lastModified": 1717284937,
"narHash": "sha256-lIbdfCsf8LMFloheeE6N31+BMIeixqyQWbSr2vk79EQ=",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1767028467,
"narHash": "sha256-7G+2aXClSMaTY1ogpX14CAxjRsvyVzpE0GRwL71WO7g=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1cabc318c11299f07ca53e3cb719854682fe6eb3",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1720691131,
"narHash": "sha256-CWT+KN8aTPyMIx8P303gsVxUnkinIz0a/Cmasz1jyIM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a046c1202e11b62cbede5385ba64908feb7bfac4",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-22.05",
"ref": "nixos-24.05",
"type": "indirect"
}
},
"nixpkgs_4": {
"locked": {
"lastModified": 1673785507,
"narHash": "sha256-EPUT8yVdvJhhjhbgnFWXXd4IUPKSOmww2+z4AmOdyPI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d06d765eeac716d8f1ca80f0935fd6fc951816ad",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_5": {
"locked": {
"lastModified": 1767028467,
"narHash": "sha256-7G+2aXClSMaTY1ogpX14CAxjRsvyVzpE0GRwL71WO7g=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1cabc318c11299f07ca53e3cb719854682fe6eb3",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_6": {
"locked": {
"lastModified": 1770464364,
"narHash": "sha256-z5NJPSBwsLf/OfD8WTmh79tlSU8XgIbwmk6qB1/TFzY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "23d72dabcb3b12469f57b37170fcbc1789bd7457",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-25.11",
"type": "indirect"
}
},
"root": {
"inputs": {
"clj-nix": "clj-nix",
"nixpkgs": "nixpkgs",
"utils": "utils"
"fudo-clojure": "fudo-clojure",
"helpers": "helpers_2",
"nixpkgs": "nixpkgs_6",
"utils": "utils_6"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_3": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_4": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": {
@@ -110,6 +597,93 @@
"repo": "flake-utils",
"type": "github"
}
},
"utils_2": {
"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"
}
},
"utils_3": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"utils_4": {
"locked": {
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"utils_5": {
"inputs": {
"systems": "systems_3"
},
"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"
}
},
"utils_6": {
"inputs": {
"systems": "systems_4"
},
"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"
}
}
},
"root": "root",

View File

@@ -2,36 +2,45 @@
description = "WallFly presence monitor.";
inputs = {
nixpkgs.url = "nixpkgs/nixos-22.05";
nixpkgs.url = "nixpkgs/nixos-25.11";
utils.url = "github:numtide/flake-utils";
clj-nix = {
url = "github:jlesquembre/clj-nix";
helpers = {
url = "github:fudoniten/fudo-nix-helpers";
inputs.nixpkgs.follows = "nixpkgs";
};
fudo-clojure.url = "github:fudoniten/fudo-clojure";
};
outputs = { self, nixpkgs, utils, clj-nix, ... }:
outputs = { self, nixpkgs, utils, helpers, fudo-clojure, ... }:
utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages."${system}";
cljpkgs = clj-nix.packages."${system}";
update-deps = pkgs.writeShellScriptBin "update-deps.sh" ''
${clj-nix.packages."${system}".deps-lock}/bin/deps-lock
'';
gs = import nixpkgs { inherit system; };
cljLibs = {
"org.fudo/fudo-clojure" =
fudo-clojure.packages."${system}".fudo-clojure.preppedSrc;
};
in {
packages = {
wallfly = cljpkgs.mkCljBin {
wallfly = helpers.legacyPackages."${system}".mkClojureBin {
projectSrc = ./.;
name = "org.fudo/wallfly";
main-ns = "wallfly.core";
jdkRunner = pkgs.jdk17_headless;
primaryNamespace = "wallfly.core";
src = ./.;
inherit cljLibs;
};
};
defaultPackage = self.packages."${system}".wallfly;
devShell =
pkgs.mkShell { buildInputs = with pkgs; [ clojure update-deps ]; };
devShells = rec {
default = updateDeps;
updateDeps = pkgs.mkShell {
buildInputs = with helpers.legacyPackages."${system}";
[ (updateClojureDeps { deps = cljLibs; }) ];
};
};
}) // {
overlay = final: prev: {
inherit (self.packages."${prev.system}") wallfly;

179
src/wallfly/core.clj Normal file → Executable file
View File

@@ -1,63 +1,130 @@
#!/usr/bin/env bb
(ns wallfly.core
(:require [clojure.java.shell :as shell]
(:require [babashka.process :as process]
[clojure.core.async :refer [chan >!! <!! go-loop timeout alt!]]
[clojure.string :as str :refer [trim-newline]]
[clojure.data.json :as json]
[clojure.string :as str]
[cheshire.core :as json]
[clojure.tools.cli :refer [parse-opts]]
[clojure.pprint :refer [pprint]]
[fudo-clojure.result :refer [success failure unwrap map-success success? error-message]]
[camel-snake-kebab.core :refer [->SCREAMING_SNAKE_CASE]])
(:import [org.eclipse.paho.client.mqttv3
MqttClient
MqttConnectOptions
MqttMessage]
org.eclipse.paho.client.mqttv3.persist.MemoryPersistence)
(:gen-class))
[clojure.pprint :refer [pprint]]))
(defn- exit! [status msg]
(println msg)
(System/exit status))
(defn- create-mqtt-client [broker-uri client-id mqtt-username passwd]
(let [opts (doto (MqttConnectOptions.)
(.setCleanSession true)
(.setAutomaticReconnect true)
(.setPassword (char-array passwd))
(.setUserName mqtt-username))]
(try
(doto (MqttClient. broker-uri client-id (MemoryPersistence.))
(.connect opts))
(catch Exception e
(exit! 1 (.getMessage e))))))
(defn- parse-broker-uri [uri]
"Parse MQTT broker URI like tcp://host:port or ssl://host:port"
(let [uri-pattern #"^(tcp|ssl)://([^:]+)(?::(\d+))?$"
[_ protocol host port] (re-matches uri-pattern uri)]
(when-not host
(exit! 1 (str "Invalid broker URI format: " uri)))
{:host host
:port (or port (if (= protocol "ssl") "8883" "1883"))
:use-tls (= protocol "ssl")}))
(defn- create-mqtt-config [broker-uri mqtt-username passwd]
"Create MQTT configuration for mosquitto_pub"
(merge (parse-broker-uri broker-uri)
{:username mqtt-username
:password passwd}))
(defn- shell-exec [& args]
(let [{:keys [exit out err]} (apply shell/sh args)]
(if (= exit 0)
(success (trim-newline out))
(failure err { :error err :status-code exit }))))
"Execute shell command and return {:success true :out output} or {:success false :error error}"
(try
(let [result (process/shell {:out :string :err :string} (str/join " " args))
out (str/trim (:out result))]
{:success true :out out})
(catch Exception e
{:success false :error (ex-message e)})))
(defn- get-current-session-id []
"Get the current user's session ID from loginctl"
(let [username (System/getenv "USER")
result (shell-exec "loginctl" "list-sessions" "--no-legend")]
(when (:success result)
(->> (str/split-lines (:out result))
(filter #(str/includes? % username))
first
(re-find #"^\s*(\S+)")
second))))
(defn- get-idle-time-from-loginctl []
"Get idle time in seconds using systemd-logind (works with X11 and Wayland)"
(if-let [session-id (get-current-session-id)]
(let [idle-hint-result (shell-exec "loginctl" "show-session" session-id "-p" "IdleHint" "--value")
idle-since-result (shell-exec "loginctl" "show-session" session-id "-p" "IdleSinceHintMonotonic" "--value")]
(if (and (:success idle-hint-result) (:success idle-since-result))
(if (= "yes" (:out idle-hint-result))
;; Session is idle - calculate how long
(let [idle-since-usec (Long/parseLong (:out idle-since-result))
uptime-result (shell-exec "awk" "{print $1*1000000}" "/proc/uptime")
now-usec (when (:success uptime-result) (long (Double/parseDouble (:out uptime-result))))
idle-usec (when now-usec (- now-usec idle-since-usec))
idle-sec (when idle-usec (max 0 (quot idle-usec 1000000)))]
(if idle-sec
{:success true :out idle-sec}
{:success false :error "Failed to calculate idle time"}))
;; Session is not idle
{:success true :out 0})
{:success false :error (str "Failed to get session idle info: " (:error idle-since-result))}))
{:success false :error "Could not determine current session"}))
(defn- get-idle-time-from-xprintidle []
"Get idle time in seconds using xprintidle (X11 only, legacy fallback)"
(let [result (shell-exec "xprintidle")]
(if (:success result)
{:success true :out (quot (Integer/parseInt (:out result)) 1000)}
result)))
(defn- get-idle-time []
(map-success (shell-exec "xprintidle")
(fn [idle-str] (quot (Integer/parseInt idle-str) 1000))))
"Get idle time in seconds.
Tries systemd-logind first (works with both X11 and Wayland),
falls back to xprintidle (X11 only) if loginctl fails.
This ensures compatibility with modern Linux systems that use Wayland,
while maintaining backwards compatibility with X11-only systems."
(let [result (get-idle-time-from-loginctl)]
(if (:success result)
result
(get-idle-time-from-xprintidle))))
(defn- get-hostname [] (unwrap (shell-exec "hostname")))
(defn- get-hostname []
(let [result (shell-exec "hostname")]
(if (:success result)
(:out result)
(exit! 1 (str "Failed to get hostname: " (:error result))))))
(defn- get-fqdn [] (unwrap (shell-exec "hostname" "-f")))
(defn- get-fqdn []
(let [result (shell-exec "hostname" "-f")]
(if (:success result)
(:out result)
(exit! 1 (str "Failed to get FQDN: " (:error result))))))
(defn- get-username [] (System/getenv "USER"))
(defn- create-message [msg retained]
(doto (MqttMessage. (.getBytes msg))
(.setQos 1)
(.setRetained retained)))
(defn- send-message [mqtt-config topic msg & {:keys [retained] :or {retained false}}]
"Send MQTT message using mosquitto_pub"
(let [{:keys [host port username password use-tls]} mqtt-config
base-args ["mosquitto_pub"
"-h" host
"-p" (str port)
"-u" username
"-P" password
"-t" topic
"-m" msg
"-q" "1"]
args (if retained
(conj base-args "-r")
base-args)]
(try
(let [result @(process/process args {:out :string :err :string})]
(when (not= 0 (:exit result))
(exit! 1 (str "mosquitto_pub failed: " (:err result)))))
(catch Exception e
(exit! 1 (str "Failed to send MQTT message: " (ex-message e)))))))
(defn- send-message [client topic msg & {:keys [retained] :or {retained false}}]
(try
(.publish client topic (create-message msg retained))
(catch Exception e
(exit! 1 (.getMessage e)))))
(defn- create-reporter [client time-to-idle location user host host-device]
(defn- create-reporter [mqtt-config time-to-idle location user host host-device]
(let [base-topic (format "homeassistant/binary_sensor/wallfly_%s_%s"
user host)
presence-topic (format "%s/state" base-topic)]
@@ -75,13 +142,13 @@
:manufacturer "Fudo"
:suggested_area location}}]
(println (format "sending to %s: %s" cfg-topic (with-out-str (pprint payload))))
(send-message client cfg-topic (json/write-str payload) :retained true)
(send-message mqtt-config cfg-topic (json/generate-string payload) :retained true)
;; Send one message that will persist if the host dies
(send-message client presence-topic "OFF" :retained true))
(send-message mqtt-config presence-topic "OFF" :retained true))
(fn [idle-time]
;; (emit-idle idle-time)
(when (< idle-time time-to-idle)
(send-message client presence-topic "ON")))))
(send-message mqtt-config presence-topic "ON")))))
(defn- execute! [delay-seconds report]
(let [stop-chan (chan)
@@ -89,9 +156,9 @@
(go-loop [idle-measure (get-idle-time)]
(if (nil? idle-measure)
nil
(do (if (success? idle-measure)
(report (unwrap idle-measure))
(println (str "error reading idle time: " (error-message idle-measure))))
(do (if (:success idle-measure)
(report (:out idle-measure))
(println (str "error reading idle time: " (:error idle-measure))))
(recur (alt! (timeout delay-time) (get-idle-time)
stop-chan nil)))))))
@@ -103,11 +170,15 @@
["-t" "--time-to-idle SECONDS" "Number of seconds before considering this host idle."]
["-d" "--delay-time SECONDS" "Time to wait before polling for idle time."]])
(defn- ->screaming-snake-case [s]
"Convert kebab-case string to SCREAMING_SNAKE_CASE"
(-> s (str/replace "-" "_") (str/upper-case)))
(defn- get-key [opts k]
(if-let [opt (get opts k)]
[k opt]
[k (System/getenv (format "WALLFLY_%s"
(-> k name ->SCREAMING_SNAKE_CASE)))]))
(-> k name ->screaming-snake-case)))]))
(defn- get-args [keys args]
(let [{:keys [options errors summary]} (parse-opts args cli-opts)]
@@ -137,16 +208,18 @@
delay-time
mqtt-username]} (get-args required-keys args)
catch-shutdown (chan)
password (-> mqtt-password-file (slurp) (str/trim-newline))
password (-> mqtt-password-file (slurp) (str/trim))
username (get-username)
hostname (get-hostname)
host-device (format "wallfly-%s" (get-fqdn))
client-id (format "wallfly-%s" (rand-str 10))
client (create-mqtt-client mqtt-broker-uri client-id mqtt-username password)
reporter (create-reporter client (Integer/parseInt time-to-idle) location username hostname host-device)
mqtt-config (create-mqtt-config mqtt-broker-uri mqtt-username password)
reporter (create-reporter mqtt-config (Integer/parseInt time-to-idle) location username hostname host-device)
stop-chan (execute! (Integer/parseInt delay-time) reporter)]
(.addShutdownHook (Runtime/getRuntime)
(Thread. (fn [] (>!! catch-shutdown true))))
(<!! catch-shutdown)
(>!! stop-chan true)
(System/exit 0)))
(when (= *file* (System/getProperty "babashka.file"))
(apply -main *command-line-args*))