diff --git a/.gitignore b/.gitignore index ea8c4bf..54b5d99 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,21 @@ -/target +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +# Config file +config.json diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index ba2010f..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,351 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "batmon" -version = "0.1.0" -dependencies = [ - "glob", - "notify", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "crossbeam-channel" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" - -[[package]] -name = "filetime" -version = "0.2.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "windows-sys 0.52.0", -] - -[[package]] -name = "fsevent-sys" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" -dependencies = [ - "libc", -] - -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - -[[package]] -name = "inotify" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" -dependencies = [ - "bitflags 1.3.2", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" -dependencies = [ - "libc", -] - -[[package]] -name = "kqueue" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" -dependencies = [ - "kqueue-sys", - "libc", -] - -[[package]] -name = "kqueue-sys" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - -[[package]] -name = "libc" -version = "0.2.152" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" - -[[package]] -name = "log" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" - -[[package]] -name = "mio" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.48.0", -] - -[[package]] -name = "notify" -version = "6.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" -dependencies = [ - "bitflags 2.4.1", - "crossbeam-channel", - "filetime", - "fsevent-sys", - "inotify", - "kqueue", - "libc", - "log", - "mio", - "walkdir", - "windows-sys 0.48.0", -] - -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "walkdir" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[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-util" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" -dependencies = [ - "winapi", -] - -[[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.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.0", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" -dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" diff --git a/Cargo.toml b/Cargo.toml deleted file mode 100644 index b70b342..0000000 --- a/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "batmon" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -notify = "6.1" -glob = "0.3" diff --git a/README.md b/README.md new file mode 100644 index 0000000..1f4f48d --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# 🔋 Batmon + +> **Batmon** is a dead-simple battery monitor for Linux written in Go. It +> provides real-time monitoring of battery status and adjusts the power profile +> accordingly to optimize battery life. + +## Features + +- Real-time monitoring of battery status +- Adjustment of power profile based on battery status +- Support for custom commands and extra commands +- Configuration via a JSON file +- Not written in Rust (the codebase is _readable_) + +## Installation + +## Prerequisites + +- Upower +- powerprofilesctl +- Nix or Go + +### Nix + +**Batmon** is primarily distributed through a Nix flake. You may install it +manually using `nix profile install github:NotAShelf/batmon` + +### Manually + +```console +go install . # this will install Batmon in your $GOPATH +``` + +## Usage + +To start using Batmon, use the following command: + +``` +batmon +``` + +By default, Gomon will load the configuration from config.json in the current +directory. You can specify a different configuration file using the `--config` +flag: + +```console +gomon -c /path/to/config.json +``` + +The configuration file should contain a list of batteries to monitor, along +with any custom commands or extra commands to execute. Here's an example of +a configuration file: + +```json +{ + "batPaths": [ + { + "path": "/sys/class/power_supply/BAT0", + "command": "powerprofilesctl set performance", + "extraCommand": "echo 'Battery is charging' | wall" + } + ] +} +``` + +- You can leave `command` empty to use the default behaviour - which will + switch active powerprofile using `powerprofiles set performance | balanced` + +- `extraCommand`, if provided, will be executed in addition to the `command` + value. diff --git a/TODO b/TODO deleted file mode 100644 index f11bb88..0000000 --- a/TODO +++ /dev/null @@ -1,4 +0,0 @@ -- nix integration -- unit tests -- use the battery crate -- rewrite in Go as soon as rust frustrates me enough diff --git a/default.nix b/default.nix deleted file mode 100644 index fcf1885..0000000 --- a/default.nix +++ /dev/null @@ -1,23 +0,0 @@ -{ - lib, - rustPlatform, - ... -}: let - pname = "batmon"; - version = "unstable-2024-01-08"; -in - rustPlatform.buildRustPackage { - inherit pname version; - - src = lib.cleanSource ./.; - cargoHash = "sha256-d9wWr17BnlRwa3CLcfDeby60a2BPwpBy1xjY6oTgyG0="; - - meta = { - description = "Nananananananana batmon"; - homepage = "https://github.com/NotAShelf/batmon.git"; - license = lib.licenses.gpl3Only; - maintainers = with lib.maintainers; [NotAShelf]; - mainProgram = "batmon"; - platforms = lib.platforms.linux; - }; - } diff --git a/example.config.json b/example.config.json new file mode 100644 index 0000000..dbd4010 --- /dev/null +++ b/example.config.json @@ -0,0 +1,8 @@ +{ + "batPaths": [ + { + "path": "/sys/class/power_supply/BAT0", + "extraCommand": "notify-send 'hello'" + } + ] +} diff --git a/flake.lock b/flake.lock deleted file mode 100644 index 7c737b0..0000000 --- a/flake.lock +++ /dev/null @@ -1,26 +0,0 @@ -{ - "nodes": { - "nixpkgs": { - "locked": { - "lastModified": 1704796538, - "narHash": "sha256-uRMWOijfPGHzLXMqiYGoAitkq1beLfg2JAmGLdTjmvQ=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "792a7ddec9e0fe05aed934160eb75f1a821eb69b", - "type": "github" - }, - "original": { - "owner": "NixOS", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "nixpkgs": "nixpkgs" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix index 21b4b61..a2c4450 100644 --- a/flake.nix +++ b/flake.nix @@ -1,8 +1,6 @@ { - description = "Batmon - battery monitor service"; - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs"; - }; + description = "Batmon - battery monitor"; + inputs.nixpkgs.url = "github:NixOS/nixpkgs"; outputs = { self, @@ -13,11 +11,12 @@ pkgsForEach = nixpkgs.legacyPackages; in { packages = forEachSystem (system: { - default = pkgsForEach.${system}.callPackage ./default.nix {}; + batmon = pkgsForEach.${system}.callPackage ./nix/package.nix {}; + default = self.${system}.batmon; }); devShells = forEachSystem (system: { - default = pkgsForEach.${system}.callPackage ./shell.nix {}; + default = pkgsForEach.${system}.callPackage ./nix/shell.nix {}; }); }; } diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..33e8d51 --- /dev/null +++ b/go.mod @@ -0,0 +1,29 @@ +module gomon + +go 1.21.5 + +require ( + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/cobra v1.8.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.18.2 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f4ba775 --- /dev/null +++ b/go.sum @@ -0,0 +1,62 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/battery/battery.go b/internal/battery/battery.go new file mode 100644 index 0000000..a0361e1 --- /dev/null +++ b/internal/battery/battery.go @@ -0,0 +1,71 @@ +package battery + +import ( + "os" + "strings" + "time" + + "gomon/internal/exec" + "gomon/internal/logger" + "gomon/internal/model" +) + +// battery monitor service +func Monitor(bat model.Battery, profiles ...string) error { + // wait a while if needed + startupWait := os.Getenv("STARTUP_WAIT") + if startupWait != "" { + duration, err := time.ParseDuration(startupWait) + if err != nil { + logger.Error("Invalid STARTUP_WAIT duration") + return nil + } + time.Sleep(duration) + } + + // start the monitor loop + var prevProfile string + for { + // read the current state + batteryStatus, err := os.ReadFile(bat.Path + "/status") + if err != nil { + logger.Error("Failed to read battery status") + return nil + } + currentProfile := "performance" + if strings.TrimSpace(string(batteryStatus)) == "Discharging" { + currentProfile = "balanced" + } + + // set the new profile + if currentProfile != prevProfile { + logger.Info("Setting power profile to %s for battery %s", currentProfile, bat.Path) + var commandArgs []string + if bat.Command != "" { + commandArgs = strings.Split(bat.Command, " ") + } else { + commandArgs = []string{"powerprofilesctl", "set", currentProfile} + } + err = exec.ExecCommand(commandArgs[0], commandArgs[1:]...) + if err != nil { + logger.Error("Failed to execute command") + return nil + } + + // execute the extra command + // if any + if bat.ExtraCommand != "" { + extraCommandArgs := strings.Split(bat.ExtraCommand, " ") + err = exec.ExecCommand(extraCommandArgs[0], extraCommandArgs[1:]...) + if err != nil { + logger.Error("Failed to execute extra command") + return nil + } + } + } + prevProfile = currentProfile + + // wait for the next power change event + time.Sleep(1 * time.Second) + } +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..6d307ce --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,29 @@ +package config + +import ( + "encoding/json" + "os" + + "gomon/internal/logger" + "gomon/internal/model" +) + +// Load loads the battery configuration from a file. +func Load(filename string) (model.BatteryConfig, error) { + file, err := os.Open(filename) + if err != nil { + logger.Error("Failed to open file") + return model.BatteryConfig{}, err + } + defer file.Close() + + var config model.BatteryConfig + decoder := json.NewDecoder(file) + err = decoder.Decode(&config) + if err != nil { + logger.Error("Failed to decode JSON") + return model.BatteryConfig{}, err + } + + return config, nil +} diff --git a/internal/exec/exec.go b/internal/exec/exec.go new file mode 100644 index 0000000..706be70 --- /dev/null +++ b/internal/exec/exec.go @@ -0,0 +1,21 @@ +package exec + +import ( + "os/exec" + + "gomon/internal/logger" +) + +// execute a command with args and returns its output +// this will be used primarily for executing external commands if configured +// or executing bash commands verbatim +func ExecCommand(name string, arg ...string) error { + cmd := exec.Command(name, arg...) + err := cmd.Run() + if err != nil { + logger.Error("Failed to execute command") + return err + } + + return nil +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..b351f17 --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,24 @@ +package logger + +import ( + "github.com/sirupsen/logrus" +) + +var Log *logrus.Logger + +func init() { + Log = logrus.New() + Log.SetFormatter(&logrus.JSONFormatter{}) +} + +func Info(format string, v ...interface{}) { + Log.Infof(format, v...) +} + +func Error(format string, v ...interface{}) { + Log.Errorf(format, v...) +} + +func Fatal(format string, v ...interface{}) { + Log.Fatalf(format, v...) +} diff --git a/internal/model/model.go b/internal/model/model.go new file mode 100644 index 0000000..030737e --- /dev/null +++ b/internal/model/model.go @@ -0,0 +1,11 @@ +package model + +type Battery struct { + Path string `json:"path"` + Command string `json:"command"` + ExtraCommand string `json:"extraCommand"` +} + +type BatteryConfig struct { + BatPaths []Battery `json:"batPaths"` +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..85948cf --- /dev/null +++ b/main.go @@ -0,0 +1,60 @@ +package main + +import ( + "sync" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "gomon/internal/battery" + "gomon/internal/config" + "gomon/internal/model" +) + +func main() { + var rootCmd = &cobra.Command{ + Use: "gomon", + Short: "TODO", + Long: `TODO`, + Run: func(cmd *cobra.Command, args []string) { + // load the battery configuration + config, err := config.Load(viper.GetString("config")) + if err != nil { + logrus.WithFields(logrus.Fields{ + "error": err, + }).Fatal("Failed to load configuration") + } + + // create a WaitGroup to wait for all goroutines to finish + var wg sync.WaitGroup + wg.Add(len(config.BatPaths)) + + // start a goroutine for each battery + for _, bat := range config.BatPaths { + go func(bat model.Battery) { + defer wg.Done() + err := battery.Monitor(bat) + if err != nil { + logrus.WithFields(logrus.Fields{ + "error": err, + "battery": bat.Path, + }).Error("Failed to monitor battery") + } + }(bat) + } + + // wait for all goroutines to finish + wg.Wait() + }, + } + + rootCmd.PersistentFlags().StringP("config", "c", "config.json", "Path to the configuration file") + viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config")) + + if err := rootCmd.Execute(); err != nil { + logrus.WithFields(logrus.Fields{ + "error": err, + }).Fatal("Failed to execute root command") + } +} diff --git a/nix/package.nix b/nix/package.nix new file mode 100644 index 0000000..6984378 --- /dev/null +++ b/nix/package.nix @@ -0,0 +1,11 @@ +{buildGoModule}: +buildGoModule { + pname = "sample-go"; + version = "0.0.1"; + + src = ./.; + + vendorHash = null; + + ldflags = ["-s" "-w"]; +} diff --git a/nix/shell.nix b/nix/shell.nix new file mode 100644 index 0000000..52633eb --- /dev/null +++ b/nix/shell.nix @@ -0,0 +1,15 @@ +{ + callPackage, + gopls, + go, +}: let + mainPkg = callPackage ./default.nix {}; +in + mainPkg.overrideAttrs (oa: { + nativeBuildInputs = + (oa.nativeBuildInputs or []) + ++ [ + gopls + go + ]; + }) diff --git a/shell.nix b/shell.nix deleted file mode 100644 index 8150363..0000000 --- a/shell.nix +++ /dev/null @@ -1,20 +0,0 @@ -{ - callPackage, - rust-analyzer, - rustfmt, - clippy, - cargo, -}: let - mainPkg = callPackage ./default.nix {}; -in - mainPkg.overrideAttrs (oa: { - nativeBuildInputs = - [ - # Additional rust tooling - rust-analyzer - rustfmt - clippy - cargo - ] - ++ (oa.nativeBuildInputs or []); - }) diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 9f456c6..0000000 --- a/src/main.rs +++ /dev/null @@ -1,111 +0,0 @@ -use std::fs; -use std::path::Path; -use std::thread; -use std::time::Duration; -use std::sync::mpsc::channel; -use std::process::Command; -use std::error::Error; -use notify::{RecommendedWatcher, RecursiveMode, Watcher, Config}; -use glob::glob; - -use std::os::unix::fs::PermissionsExt; - -// check if the current user has read permissions for a given path -fn has_read_permission>(path: P) -> Result> { - let metadata = fs::metadata(path.as_ref())?; - let permissions = metadata.permissions(); - let mode = permissions.mode(); - Ok(mode & 0o444 != 0) -} - - - -fn main() -> Result<(), Box> { - println!("Starting the battery monitor..."); - - let bat_base_paths = glob("/sys/class/power_supply/BAT*")?.filter_map(Result::ok); - - let mut bat_status_path = String::new(); - let mut bat_capacity_path = String::new(); - - for path in bat_base_paths { - println!("Found battery path: {:?}", path.display()); - bat_status_path = format!("{}/status", path.display()); - bat_capacity_path = format!("{}/capacity", path.display()); - // break after finding the first match. - // FIXME: this is assuming there's only one battery, account for multiple? - break; - } - - if bat_status_path.is_empty() || bat_capacity_path.is_empty() { - return Err("Battery path not found".into()); - } - - // check if we have read permissions for the battery status and capacity paths - // TODO: I'm pretty sure there's a better and faster way to do this - if !has_read_permission(&bat_status_path)? { - println!("No read permission for the battery status path"); - return Err("Permission denied".into()); - } - - if !has_read_permission(&bat_capacity_path)? { - println!("No read permission for the battery capacity path"); - return Err("Permission denied".into()); - } - - println!("Battery status path: {}", bat_status_path); - println!("Battery capacity path: {}", bat_capacity_path); - - let ac_profile = "performance"; - let bat_profile = "balanced"; - - if let Ok(wait_time_str) = std::env::var("STARTUP_WAIT") { - println!("STARTUP_WAIT is set, waiting for {} seconds", wait_time_str); - if let Ok(wait_time) = wait_time_str.parse::() { - thread::sleep(Duration::from_secs(wait_time)); - } else { - println!("Invalid STARTUP_WAIT value: {}", wait_time_str); - } - } - - let mut prev_profile = String::new(); - - let (tx, rx) = channel(); - let mut watcher = RecommendedWatcher::new(tx, Config::default())?; - - println!("Watching battery status and capacity paths for changes..."); - watcher.watch(Path::new(&bat_status_path), RecursiveMode::NonRecursive)?; - watcher.watch(Path::new(&bat_capacity_path), RecursiveMode::NonRecursive)?; - - loop { - match rx.recv() { - Ok(_) => { - println!("Change detected in battery status or capacity."); - let current_status = fs::read_to_string(&bat_status_path)?.trim().to_string(); - println!("Current battery status: {}", current_status); - - let profile = if current_status == "Discharging" { - bat_profile - } else { - ac_profile - }; - - if prev_profile != profile { - println!("Setting power profile to {}", profile); - let output = Command::new("powerprofilesctl") - .arg("set") - .arg(profile) - .output()?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - println!("Failed to set power profile: {}", stderr); - } - } - prev_profile = profile.to_string(); - }, - Err(e) => println!("Watch error: {:?}", e), - } - } -} -