mirror of
https://github.com/RGBCube/superfreq
synced 2025-07-27 17:07:44 +00:00
initial commit
Guess which idiot accidentally deleted the .git directory
This commit is contained in:
commit
8285210557
9 changed files with 1646 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
target*
|
||||||
|
result*
|
474
Cargo.lock
generated
Normal file
474
Cargo.lock
generated
Normal file
|
@ -0,0 +1,474 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstream"
|
||||||
|
version = "0.6.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"anstyle-parse",
|
||||||
|
"anstyle-query",
|
||||||
|
"anstyle-wincon",
|
||||||
|
"colorchoice",
|
||||||
|
"is_terminal_polyfill",
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle"
|
||||||
|
version = "1.0.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-parse"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
|
||||||
|
dependencies = [
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-query"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-wincon"
|
||||||
|
version = "3.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"once_cell",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap"
|
||||||
|
version = "4.5.38"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
|
||||||
|
dependencies = [
|
||||||
|
"clap_builder",
|
||||||
|
"clap_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_builder"
|
||||||
|
version = "4.5.38"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
|
||||||
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
|
"anstyle",
|
||||||
|
"clap_lex",
|
||||||
|
"strsim",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_derive"
|
||||||
|
version = "4.5.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_lex"
|
||||||
|
version = "0.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorchoice"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||||
|
dependencies = [
|
||||||
|
"dirs-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs-sys"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"option-ext",
|
||||||
|
"redox_users",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.15.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hermit-abi"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is_terminal_polyfill"
|
||||||
|
version = "1.70.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.172"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libredox"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_cpus"
|
||||||
|
version = "1.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
|
||||||
|
dependencies = [
|
||||||
|
"hermit-abi",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "option-ext"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.95"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||||
|
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 = "redox_users"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
"libredox",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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_spanned"
|
||||||
|
version = "0.6.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "superfreq"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
"dirs",
|
||||||
|
"num_cpus",
|
||||||
|
"serde",
|
||||||
|
"toml",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.101"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "2.0.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "2.0.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.8.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_edit",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.6.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.22.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_write",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_write"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.0+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||||
|
|
||||||
|
[[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 = "winnow"
|
||||||
|
version = "0.7.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
11
Cargo.toml
Normal file
11
Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
[package]
|
||||||
|
name = "superfreq"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
toml = "0.8"
|
||||||
|
dirs = "6.0"
|
||||||
|
clap = { version = "4.0", features = ["derive"] }
|
||||||
|
num_cpus = "1.16"
|
197
src/config.rs
Normal file
197
src/config.rs
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::fs;
|
||||||
|
use crate::core::{OperationalMode, TurboSetting};
|
||||||
|
|
||||||
|
// Structs for configuration using serde::Deserialize
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
pub struct ProfileConfig {
|
||||||
|
pub governor: Option<String>,
|
||||||
|
pub turbo: Option<TurboSetting>,
|
||||||
|
pub epp: Option<String>, // Energy Performance Preference (EPP)
|
||||||
|
pub epb: Option<String>, // Energy Performance Bias (EPB) - usually an integer, but string for flexibility from sysfs
|
||||||
|
pub min_freq_mhz: Option<u32>,
|
||||||
|
pub max_freq_mhz: Option<u32>,
|
||||||
|
pub platform_profile: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ProfileConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
ProfileConfig {
|
||||||
|
governor: Some("schedutil".to_string()), // common sensible default (?)
|
||||||
|
turbo: Some(TurboSetting::Auto),
|
||||||
|
epp: None, // defaults depend on governor and system
|
||||||
|
epb: None, // defaults depend on governor and system
|
||||||
|
min_freq_mhz: None, // no override
|
||||||
|
max_freq_mhz: None, // no override
|
||||||
|
platform_profile: None, // no override
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Default, Clone)]
|
||||||
|
pub struct AppConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
pub charger: ProfileConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub battery: ProfileConfig,
|
||||||
|
pub battery_charge_thresholds: Option<(u8, u8)>, // (start_threshold, stop_threshold)
|
||||||
|
pub ignored_power_supplies: Option<Vec<String>>,
|
||||||
|
#[serde(default = "default_poll_interval_sec")]
|
||||||
|
pub poll_interval_sec: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_poll_interval_sec() -> u64 {
|
||||||
|
5
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error type for config loading
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ConfigError {
|
||||||
|
Io(std::io::Error),
|
||||||
|
Toml(toml::de::Error),
|
||||||
|
NoValidConfigFound,
|
||||||
|
HomeDirNotFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for ConfigError {
|
||||||
|
fn from(err: std::io::Error) -> ConfigError {
|
||||||
|
ConfigError::Io(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<toml::de::Error> for ConfigError {
|
||||||
|
fn from(err: toml::de::Error) -> ConfigError {
|
||||||
|
ConfigError::Toml(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ConfigError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ConfigError::Io(e) => write!(f, "I/O error: {}", e),
|
||||||
|
ConfigError::Toml(e) => write!(f, "TOML parsing error: {}", e),
|
||||||
|
ConfigError::NoValidConfigFound => write!(f, "No valid configuration file found."),
|
||||||
|
ConfigError::HomeDirNotFound => write!(f, "Could not determine user home directory."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for ConfigError {}
|
||||||
|
|
||||||
|
// The primary function to load application configuration.
|
||||||
|
// It tries user-specific and then system-wide TOML files.
|
||||||
|
// Falls back to default settings if no file is found or if parsing fails.
|
||||||
|
pub fn load_config() -> Result<AppConfig, ConfigError> {
|
||||||
|
let mut config_paths: Vec<PathBuf> = Vec::new();
|
||||||
|
|
||||||
|
// User-specific path
|
||||||
|
if let Some(home_dir) = dirs::home_dir() {
|
||||||
|
let user_config_path = home_dir.join(".config/auto_cpufreq_rs/config.toml");
|
||||||
|
config_paths.push(user_config_path);
|
||||||
|
} else {
|
||||||
|
eprintln!("Warning: Could not determine home directory. User-specific config will not be loaded.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// System-wide path
|
||||||
|
let system_config_path = PathBuf::from("/etc/auto_cpufreq_rs/config.toml");
|
||||||
|
config_paths.push(system_config_path);
|
||||||
|
|
||||||
|
for path in config_paths {
|
||||||
|
if path.exists() {
|
||||||
|
println!("Attempting to load config from: {}", path.display());
|
||||||
|
match fs::read_to_string(&path) {
|
||||||
|
Ok(contents) => {
|
||||||
|
match toml::from_str::<AppConfigToml>(&contents) {
|
||||||
|
Ok(toml_app_config) => {
|
||||||
|
// Convert AppConfigToml to AppConfig
|
||||||
|
let app_config = AppConfig {
|
||||||
|
charger: ProfileConfig::from(toml_app_config.charger),
|
||||||
|
battery: ProfileConfig::from(toml_app_config.battery),
|
||||||
|
battery_charge_thresholds: toml_app_config.battery_charge_thresholds,
|
||||||
|
ignored_power_supplies: toml_app_config.ignored_power_supplies,
|
||||||
|
poll_interval_sec: toml_app_config.poll_interval_sec,
|
||||||
|
};
|
||||||
|
return Ok(app_config);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error parsing config file {}: {}", path.display(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error reading config file {}: {}", path.display(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("No configuration file found or all failed to parse. Using default configuration.");
|
||||||
|
// Construct default AppConfig by converting default AppConfigToml
|
||||||
|
let default_toml_config = AppConfigToml::default();
|
||||||
|
Ok(AppConfig {
|
||||||
|
charger: ProfileConfig::from(default_toml_config.charger),
|
||||||
|
battery: ProfileConfig::from(default_toml_config.battery),
|
||||||
|
battery_charge_thresholds: default_toml_config.battery_charge_thresholds,
|
||||||
|
ignored_power_supplies: default_toml_config.ignored_power_supplies,
|
||||||
|
poll_interval_sec: default_toml_config.poll_interval_sec,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intermediate structs for TOML parsing
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
pub struct ProfileConfigToml {
|
||||||
|
pub governor: Option<String>,
|
||||||
|
pub turbo: Option<String>, // "always", "auto", "never"
|
||||||
|
pub epp: Option<String>,
|
||||||
|
pub epb: Option<String>,
|
||||||
|
pub min_freq_mhz: Option<u32>,
|
||||||
|
pub max_freq_mhz: Option<u32>,
|
||||||
|
pub platform_profile: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone, Default)]
|
||||||
|
pub struct AppConfigToml {
|
||||||
|
#[serde(default)]
|
||||||
|
pub charger: ProfileConfigToml,
|
||||||
|
#[serde(default)]
|
||||||
|
pub battery: ProfileConfigToml,
|
||||||
|
pub battery_charge_thresholds: Option<(u8, u8)>,
|
||||||
|
pub ignored_power_supplies: Option<Vec<String>>,
|
||||||
|
#[serde(default = "default_poll_interval_sec")]
|
||||||
|
pub poll_interval_sec: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ProfileConfigToml {
|
||||||
|
fn default() -> Self {
|
||||||
|
ProfileConfigToml {
|
||||||
|
governor: Some("schedutil".to_string()),
|
||||||
|
turbo: Some("auto".to_string()),
|
||||||
|
epp: None,
|
||||||
|
epb: None,
|
||||||
|
min_freq_mhz: None,
|
||||||
|
max_freq_mhz: None,
|
||||||
|
platform_profile: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl From<ProfileConfigToml> for ProfileConfig {
|
||||||
|
fn from(toml_config: ProfileConfigToml) -> Self {
|
||||||
|
ProfileConfig {
|
||||||
|
governor: toml_config.governor,
|
||||||
|
turbo: toml_config.turbo.and_then(|s| match s.to_lowercase().as_str() {
|
||||||
|
"always" => Some(TurboSetting::Always),
|
||||||
|
"auto" => Some(TurboSetting::Auto),
|
||||||
|
"never" => Some(TurboSetting::Never),
|
||||||
|
_ => None,
|
||||||
|
}),
|
||||||
|
epp: toml_config.epp,
|
||||||
|
epb: toml_config.epb,
|
||||||
|
min_freq_mhz: toml_config.min_freq_mhz,
|
||||||
|
max_freq_mhz: toml_config.max_freq_mhz,
|
||||||
|
platform_profile: toml_config.platform_profile,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
70
src/core.rs
Normal file
70
src/core.rs
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
pub struct SystemInfo {
|
||||||
|
// Overall system details
|
||||||
|
pub cpu_model: String,
|
||||||
|
pub architecture: String,
|
||||||
|
pub linux_distribution: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CpuCoreInfo {
|
||||||
|
// Per-core data
|
||||||
|
pub core_id: u32,
|
||||||
|
pub current_frequency_mhz: Option<u32>,
|
||||||
|
pub min_frequency_mhz: Option<u32>,
|
||||||
|
pub max_frequency_mhz: Option<u32>,
|
||||||
|
pub usage_percent: Option<f32>,
|
||||||
|
pub temperature_celsius: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CpuGlobalInfo {
|
||||||
|
// System-wide CPU settings
|
||||||
|
pub current_governor: Option<String>,
|
||||||
|
pub available_governors: Vec<String>,
|
||||||
|
pub turbo_status: Option<bool>, // true for enabled, false for disabled
|
||||||
|
pub epp: Option<String>, // Energy Performance Preference
|
||||||
|
pub epb: Option<String>, // Energy Performance Bias
|
||||||
|
pub platform_profile: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BatteryInfo {
|
||||||
|
// Battery status (AC connected, charging state, capacity, power rate, charge start/stop thresholds if available).
|
||||||
|
pub name: String,
|
||||||
|
pub ac_connected: bool,
|
||||||
|
pub charging_state: Option<String>, // e.g., "Charging", "Discharging", "Full"
|
||||||
|
pub capacity_percent: Option<u8>,
|
||||||
|
pub power_rate_watts: Option<f32>, // positive for charging, negative for discharging
|
||||||
|
pub charge_start_threshold: Option<u8>,
|
||||||
|
pub charge_stop_threshold: Option<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SystemLoad {
|
||||||
|
// System load averages.
|
||||||
|
pub load_avg_1min: f32,
|
||||||
|
pub load_avg_5min: f32,
|
||||||
|
pub load_avg_15min: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SystemReport {
|
||||||
|
// Now combine all the above for a snapshot of the system state.
|
||||||
|
pub system_info: SystemInfo,
|
||||||
|
pub cpu_cores: Vec<CpuCoreInfo>,
|
||||||
|
pub cpu_global: CpuGlobalInfo,
|
||||||
|
pub batteries: Vec<BatteryInfo>,
|
||||||
|
pub system_load: SystemLoad,
|
||||||
|
pub timestamp: std::time::SystemTime, // so we know when the report was generated
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum OperationalMode {
|
||||||
|
Powersave,
|
||||||
|
Performance,
|
||||||
|
}
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
use clap::ValueEnum;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, ValueEnum)]
|
||||||
|
pub enum TurboSetting {
|
||||||
|
Always, // turbo is forced on (if possible)
|
||||||
|
Auto, // system or driver controls turbo
|
||||||
|
Never, // turbo is forced off
|
||||||
|
}
|
233
src/cpu.rs
Normal file
233
src/cpu.rs
Normal file
|
@ -0,0 +1,233 @@
|
||||||
|
use crate::core::TurboSetting;
|
||||||
|
use crate::monitor::Result as MonitorResult;
|
||||||
|
use std::{
|
||||||
|
fs, io,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ControlError {
|
||||||
|
Io(io::Error),
|
||||||
|
WriteError(String),
|
||||||
|
InvalidValueError(String),
|
||||||
|
NotSupported(String),
|
||||||
|
PermissionDenied(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<io::Error> for ControlError {
|
||||||
|
fn from(err: io::Error) -> ControlError {
|
||||||
|
match err.kind() {
|
||||||
|
io::ErrorKind::PermissionDenied => ControlError::PermissionDenied(err.to_string()),
|
||||||
|
_ => ControlError::Io(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ControlError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ControlError::Io(e) => write!(f, "I/O error: {}", e),
|
||||||
|
ControlError::WriteError(s) => write!(f, "Failed to write to sysfs path: {}", s),
|
||||||
|
ControlError::InvalidValueError(s) => write!(f, "Invalid value for setting: {}", s),
|
||||||
|
ControlError::NotSupported(s) => write!(f, "Control action not supported: {}", s),
|
||||||
|
ControlError::PermissionDenied(s) => {
|
||||||
|
write!(f, "Permission denied: {}. Try running with sudo.", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for ControlError {}
|
||||||
|
|
||||||
|
pub type Result<T, E = ControlError> = std::result::Result<T, E>;
|
||||||
|
|
||||||
|
// Write a value to a sysfs file
|
||||||
|
fn write_sysfs_value(path: impl AsRef<Path>, value: &str) -> Result<()> {
|
||||||
|
let p = path.as_ref();
|
||||||
|
fs::write(p, value).map_err(|e| {
|
||||||
|
let error_msg = format!("Path: {:?}, Value: '{}', Error: {}", p.display(), value, e);
|
||||||
|
if e.kind() == io::ErrorKind::PermissionDenied {
|
||||||
|
ControlError::PermissionDenied(error_msg)
|
||||||
|
} else {
|
||||||
|
ControlError::WriteError(error_msg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn for_each_cpu_core<F>(mut action: F) -> Result<()>
|
||||||
|
where
|
||||||
|
F: FnMut(u32) -> Result<()>,
|
||||||
|
{
|
||||||
|
// Using num_cpus::get() for a reliable count of logical cores accessible.
|
||||||
|
// The monitor module's get_logical_core_count might be more specific to cpufreq-capable cores,
|
||||||
|
// but for applying settings, we might want to iterate over all reported by OS.
|
||||||
|
// However, settings usually apply to cores with cpufreq.
|
||||||
|
// Let's use a similar discovery to monitor's get_logical_core_count
|
||||||
|
let mut cores_to_act_on = Vec::new();
|
||||||
|
let path = Path::new("/sys/devices/system/cpu");
|
||||||
|
if path.exists() {
|
||||||
|
if let Ok(entries) = fs::read_dir(path) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let name = entry.file_name();
|
||||||
|
if let Some(name_str) = name.to_str() {
|
||||||
|
if name_str.starts_with("cpu")
|
||||||
|
&& name_str.len() > 3
|
||||||
|
&& name_str[3..].chars().all(char::is_numeric)
|
||||||
|
{
|
||||||
|
if entry.path().join("cpufreq").exists() {
|
||||||
|
if let Ok(core_id) = name_str[3..].parse::<u32>() {
|
||||||
|
cores_to_act_on.push(core_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cores_to_act_on.is_empty() {
|
||||||
|
// Fallback if sysfs iteration above fails to find any cpufreq cores
|
||||||
|
let num_cores = num_cpus::get() as u32;
|
||||||
|
for core_id in 0..num_cores {
|
||||||
|
cores_to_act_on.push(core_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for core_id in cores_to_act_on {
|
||||||
|
action(core_id)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_governor(governor: &str, core_id: Option<u32>) -> Result<()> {
|
||||||
|
let action = |id: u32| {
|
||||||
|
let path = format!("/sys/devices/system/cpu/cpu{}/cpufreq/scaling_governor", id);
|
||||||
|
if Path::new(&path).exists() {
|
||||||
|
write_sysfs_value(&path, governor)
|
||||||
|
} else {
|
||||||
|
// Silently ignore if the path doesn't exist for a specific core,
|
||||||
|
// as not all cores might have cpufreq (e.g. offline cores)
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(id) = core_id {
|
||||||
|
action(id)
|
||||||
|
} else {
|
||||||
|
for_each_cpu_core(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_turbo(setting: TurboSetting) -> Result<()> {
|
||||||
|
let value_pstate = match setting {
|
||||||
|
TurboSetting::Always => "0", // no_turbo = 0 means turbo is enabled
|
||||||
|
TurboSetting::Never => "1", // no_turbo = 1 means turbo is disabled
|
||||||
|
TurboSetting::Auto => return Err(ControlError::InvalidValueError("Turbo Auto cannot be directly set via intel_pstate/no_turbo or cpufreq/boost. System default.".to_string())),
|
||||||
|
};
|
||||||
|
let value_boost = match setting {
|
||||||
|
TurboSetting::Always => "1", // boost = 1 means turbo is enabled
|
||||||
|
TurboSetting::Never => "0", // boost = 0 means turbo is disabled
|
||||||
|
TurboSetting::Auto => return Err(ControlError::InvalidValueError("Turbo Auto cannot be directly set via intel_pstate/no_turbo or cpufreq/boost. System default.".to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let pstate_path = "/sys/devices/system/cpu/intel_pstate/no_turbo";
|
||||||
|
let boost_path = "/sys/devices/system/cpu/cpufreq/boost";
|
||||||
|
|
||||||
|
if Path::new(pstate_path).exists() {
|
||||||
|
write_sysfs_value(pstate_path, value_pstate)
|
||||||
|
} else if Path::new(boost_path).exists() {
|
||||||
|
write_sysfs_value(boost_path, value_boost)
|
||||||
|
} else {
|
||||||
|
Err(ControlError::NotSupported(
|
||||||
|
"Neither intel_pstate/no_turbo nor cpufreq/boost found.".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_epp(epp: &str, core_id: Option<u32>) -> Result<()> {
|
||||||
|
let action = |id: u32| {
|
||||||
|
let path = format!(
|
||||||
|
"/sys/devices/system/cpu/cpu{}/cpufreq/energy_performance_preference",
|
||||||
|
id
|
||||||
|
);
|
||||||
|
if Path::new(&path).exists() {
|
||||||
|
write_sysfs_value(&path, epp)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(id) = core_id {
|
||||||
|
action(id)
|
||||||
|
} else {
|
||||||
|
for_each_cpu_core(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_epb(epb: &str, core_id: Option<u32>) -> Result<()> {
|
||||||
|
// EPB is often an integer 0-15. Ensure `epb` string is valid if parsing.
|
||||||
|
// For now, writing it directly as a string.
|
||||||
|
let action = |id: u32| {
|
||||||
|
let path = format!(
|
||||||
|
"/sys/devices/system/cpu/cpu{}/cpufreq/energy_performance_bias",
|
||||||
|
id
|
||||||
|
);
|
||||||
|
if Path::new(&path).exists() {
|
||||||
|
write_sysfs_value(&path, epb)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(id) = core_id {
|
||||||
|
action(id)
|
||||||
|
} else {
|
||||||
|
for_each_cpu_core(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_min_frequency(freq_mhz: u32, core_id: Option<u32>) -> Result<()> {
|
||||||
|
let freq_khz_str = (freq_mhz * 1000).to_string();
|
||||||
|
let action = |id: u32| {
|
||||||
|
let path = format!("/sys/devices/system/cpu/cpu{}/cpufreq/scaling_min_freq", id);
|
||||||
|
if Path::new(&path).exists() {
|
||||||
|
write_sysfs_value(&path, &freq_khz_str)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(id) = core_id {
|
||||||
|
action(id)
|
||||||
|
} else {
|
||||||
|
for_each_cpu_core(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_max_frequency(freq_mhz: u32, core_id: Option<u32>) -> Result<()> {
|
||||||
|
let freq_khz_str = (freq_mhz * 1000).to_string();
|
||||||
|
let action = |id: u32| {
|
||||||
|
let path = format!("/sys/devices/system/cpu/cpu{}/cpufreq/scaling_max_freq", id);
|
||||||
|
if Path::new(&path).exists() {
|
||||||
|
write_sysfs_value(&path, &freq_khz_str)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(id) = core_id {
|
||||||
|
action(id)
|
||||||
|
} else {
|
||||||
|
for_each_cpu_core(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_platform_profile(profile: &str) -> Result<()> {
|
||||||
|
let path = "/sys/firmware/acpi/platform_profile";
|
||||||
|
if Path::new(path).exists() {
|
||||||
|
// Before writing, it'd be nice to check if the profile is in available_profiles
|
||||||
|
// Reading available profiles: /sys/firmware/acpi/platform_profile_choices
|
||||||
|
// TODO: Validate profile against available profiles here for a cleaner impl.
|
||||||
|
write_sysfs_value(path, profile)
|
||||||
|
} else {
|
||||||
|
Err(ControlError::NotSupported(
|
||||||
|
"Platform profile control not found at /sys/firmware/acpi/platform_profile."
|
||||||
|
.to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
114
src/engine.rs
Normal file
114
src/engine.rs
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
use crate::core::{SystemReport, OperationalMode, TurboSetting};
|
||||||
|
use crate::config::{AppConfig, ProfileConfig};
|
||||||
|
use crate::cpu::{self, ControlError};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum EngineError {
|
||||||
|
ControlError(ControlError),
|
||||||
|
ConfigurationError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ControlError> for EngineError {
|
||||||
|
fn from(err: ControlError) -> Self {
|
||||||
|
EngineError::ControlError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for EngineError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
EngineError::ControlError(e) => write!(f, "CPU control error: {}", e),
|
||||||
|
EngineError::ConfigurationError(s) => write!(f, "Configuration error: {}", s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for EngineError {
|
||||||
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
EngineError::ControlError(e) => Some(e),
|
||||||
|
EngineError::ConfigurationError(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines the appropriate CPU profile based on power status or forced mode,
|
||||||
|
/// and applies the settings using functions from the `cpu` module.
|
||||||
|
pub fn determine_and_apply_settings(
|
||||||
|
report: &SystemReport,
|
||||||
|
config: &AppConfig,
|
||||||
|
force_mode: Option<OperationalMode>,
|
||||||
|
) -> Result<(), EngineError> {
|
||||||
|
let selected_profile_config: &ProfileConfig;
|
||||||
|
|
||||||
|
if let Some(mode) = force_mode {
|
||||||
|
match mode {
|
||||||
|
OperationalMode::Powersave => {
|
||||||
|
println!("Engine: Forced Powersave mode selected. Applying 'battery' profile.");
|
||||||
|
selected_profile_config = &config.battery;
|
||||||
|
}
|
||||||
|
OperationalMode::Performance => {
|
||||||
|
println!("Engine: Forced Performance mode selected. Applying 'charger' profile.");
|
||||||
|
selected_profile_config = &config.charger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Determine AC/Battery status
|
||||||
|
// If no batteries, assume AC power (desktop).
|
||||||
|
// Otherwise, check the ac_connected status from the (first) battery.
|
||||||
|
// XXX: This relies on the setting ac_connected in BatteryInfo being set correctly.
|
||||||
|
let on_ac_power = report.batteries.is_empty() ||
|
||||||
|
report.batteries.first().map_or(false, |b| b.ac_connected);
|
||||||
|
|
||||||
|
if on_ac_power {
|
||||||
|
println!("Engine: On AC power, selecting Charger profile.");
|
||||||
|
selected_profile_config = &config.charger;
|
||||||
|
} else {
|
||||||
|
println!("Engine: On Battery power, selecting Battery profile.");
|
||||||
|
selected_profile_config = &config.battery;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply settings from selected_profile_config
|
||||||
|
// TODO: The println! statements are for temporary debugging/logging
|
||||||
|
// and we'd like to replace them with proper logging in the future.
|
||||||
|
|
||||||
|
if let Some(governor) = &selected_profile_config.governor {
|
||||||
|
println!("Engine: Setting governor to '{}'", governor);
|
||||||
|
cpu::set_governor(governor, None)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(turbo_setting) = selected_profile_config.turbo {
|
||||||
|
println!("Engine: Setting turbo to '{:?}'", turbo_setting);
|
||||||
|
cpu::set_turbo(turbo_setting)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(epp) = &selected_profile_config.epp {
|
||||||
|
println!("Engine: Setting EPP to '{}'", epp);
|
||||||
|
cpu::set_epp(epp, None)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(epb) = &selected_profile_config.epb {
|
||||||
|
println!("Engine: Setting EPB to '{}'", epb);
|
||||||
|
cpu::set_epb(epb, None)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(min_freq) = selected_profile_config.min_freq_mhz {
|
||||||
|
println!("Engine: Setting min frequency to '{} MHz'", min_freq);
|
||||||
|
cpu::set_min_frequency(min_freq, None)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(max_freq) = selected_profile_config.max_freq_mhz {
|
||||||
|
println!("Engine: Setting max frequency to '{} MHz'", max_freq);
|
||||||
|
cpu::set_max_frequency(max_freq, None)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(profile) = &selected_profile_config.platform_profile {
|
||||||
|
println!("Engine: Setting platform profile to '{}'", profile);
|
||||||
|
cpu::set_platform_profile(profile)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Engine: Profile settings applied successfully.");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
181
src/main.rs
Normal file
181
src/main.rs
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
mod core;
|
||||||
|
mod config;
|
||||||
|
mod monitor;
|
||||||
|
mod cpu;
|
||||||
|
mod engine;
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use crate::config::AppConfig;
|
||||||
|
use crate::core::TurboSetting;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[clap(author, version, about, long_about = None)]
|
||||||
|
struct Cli {
|
||||||
|
#[clap(subcommand)]
|
||||||
|
command: Option<Commands>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
enum Commands {
|
||||||
|
/// Display current system information
|
||||||
|
Info,
|
||||||
|
/// Set CPU governor
|
||||||
|
SetGovernor {
|
||||||
|
governor: String,
|
||||||
|
#[clap(long)]
|
||||||
|
core_id: Option<u32>,
|
||||||
|
},
|
||||||
|
/// Set turbo boost behavior
|
||||||
|
SetTurbo {
|
||||||
|
#[clap(value_enum)]
|
||||||
|
setting: TurboSetting,
|
||||||
|
},
|
||||||
|
/// Set Energy Performance Preference (EPP)
|
||||||
|
SetEpp {
|
||||||
|
epp: String,
|
||||||
|
#[clap(long)]
|
||||||
|
core_id: Option<u32>,
|
||||||
|
},
|
||||||
|
/// Set Energy Performance Bias (EPB)
|
||||||
|
SetEpb {
|
||||||
|
epb: String, // Typically 0-15
|
||||||
|
#[clap(long)]
|
||||||
|
core_id: Option<u32>,
|
||||||
|
},
|
||||||
|
/// Set minimum CPU frequency
|
||||||
|
SetMinFreq {
|
||||||
|
freq_mhz: u32,
|
||||||
|
#[clap(long)]
|
||||||
|
core_id: Option<u32>,
|
||||||
|
},
|
||||||
|
/// Set maximum CPU frequency
|
||||||
|
SetMaxFreq {
|
||||||
|
freq_mhz: u32,
|
||||||
|
#[clap(long)]
|
||||||
|
core_id: Option<u32>,
|
||||||
|
},
|
||||||
|
/// Set ACPI platform profile
|
||||||
|
SetPlatformProfile {
|
||||||
|
profile: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
// Load configuration first, as it might be needed by the monitor module
|
||||||
|
// E.g., for ignored power supplies
|
||||||
|
let config = match config::load_config() {
|
||||||
|
Ok(cfg) => cfg,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error loading configuration: {}. Using default values.", e);
|
||||||
|
// Proceed with default config if loading fails, as per previous steps
|
||||||
|
AppConfig::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let command_result = match cli.command {
|
||||||
|
Some(Commands::Info) => {
|
||||||
|
match monitor::collect_system_report(&config) {
|
||||||
|
Ok(report) => {
|
||||||
|
println!("--- System Information ---");
|
||||||
|
println!("CPU Model: {}", report.system_info.cpu_model);
|
||||||
|
println!("Architecture: {}", report.system_info.architecture);
|
||||||
|
println!("Linux Distribution: {}", report.system_info.linux_distribution);
|
||||||
|
println!("Timestamp: {:?}", report.timestamp);
|
||||||
|
|
||||||
|
println!("\n--- CPU Global Info ---");
|
||||||
|
println!("Current Governor: {:?}", report.cpu_global.current_governor);
|
||||||
|
println!("Available Governors: {:?}", report.cpu_global.available_governors.join(", "));
|
||||||
|
println!("Turbo Status: {:?}", report.cpu_global.turbo_status);
|
||||||
|
println!("EPP: {:?}", report.cpu_global.epp);
|
||||||
|
println!("EPB: {:?}", report.cpu_global.epb);
|
||||||
|
println!("Platform Profile: {:?}", report.cpu_global.platform_profile);
|
||||||
|
|
||||||
|
println!("\n--- CPU Core Info ---");
|
||||||
|
for core_info in report.cpu_cores {
|
||||||
|
println!(
|
||||||
|
" Core {}: Current Freq: {:?} MHz, Min Freq: {:?} MHz, Max Freq: {:?} MHz, Usage: {:?}%, Temp: {:?}°C",
|
||||||
|
core_info.core_id,
|
||||||
|
core_info.current_frequency_mhz.map_or_else(|| "N/A".to_string(), |f| f.to_string()),
|
||||||
|
core_info.min_frequency_mhz.map_or_else(|| "N/A".to_string(), |f| f.to_string()),
|
||||||
|
core_info.max_frequency_mhz.map_or_else(|| "N/A".to_string(), |f| f.to_string()),
|
||||||
|
core_info.usage_percent.map_or_else(|| "N/A".to_string(), |f| format!("{:.1}", f)),
|
||||||
|
core_info.temperature_celsius.map_or_else(|| "N/A".to_string(), |f| format!("{:.1}", f))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("\n--- Battery Info ---");
|
||||||
|
if report.batteries.is_empty() {
|
||||||
|
println!(" No batteries found or all are ignored.");
|
||||||
|
} else {
|
||||||
|
for battery_info in report.batteries {
|
||||||
|
println!(
|
||||||
|
" Battery {}: AC Connected: {}, State: {:?}, Capacity: {:?}%, Power Rate: {:?} W, Charge Thresholds: {:?}-{:?}",
|
||||||
|
battery_info.name,
|
||||||
|
battery_info.ac_connected,
|
||||||
|
battery_info.charging_state.as_deref().unwrap_or("N/A"),
|
||||||
|
battery_info.capacity_percent.map_or_else(|| "N/A".to_string(), |c| c.to_string()),
|
||||||
|
battery_info.power_rate_watts.map_or_else(|| "N/A".to_string(), |p| format!("{:.2}", p)),
|
||||||
|
battery_info.charge_start_threshold.map_or_else(|| "N/A".to_string(), |t| t.to_string()),
|
||||||
|
battery_info.charge_stop_threshold.map_or_else(|| "N/A".to_string(), |t| t.to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("\n--- System Load ---");
|
||||||
|
println!("Load Average (1m, 5m, 15m): {:.2}, {:.2}, {:.2}",
|
||||||
|
report.system_load.load_avg_1min,
|
||||||
|
report.system_load.load_avg_5min,
|
||||||
|
report.system_load.load_avg_15min);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Commands::SetGovernor { governor, core_id }) => {
|
||||||
|
cpu::set_governor(&governor, core_id).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
|
||||||
|
}
|
||||||
|
Some(Commands::SetTurbo { setting }) => {
|
||||||
|
cpu::set_turbo(setting).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
|
||||||
|
}
|
||||||
|
Some(Commands::SetEpp { epp, core_id }) => {
|
||||||
|
cpu::set_epp(&epp, core_id).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
|
||||||
|
}
|
||||||
|
Some(Commands::SetEpb { epb, core_id }) => {
|
||||||
|
cpu::set_epb(&epb, core_id).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
|
||||||
|
}
|
||||||
|
Some(Commands::SetMinFreq { freq_mhz, core_id }) => {
|
||||||
|
cpu::set_min_frequency(freq_mhz, core_id).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
|
||||||
|
}
|
||||||
|
Some(Commands::SetMaxFreq { freq_mhz, core_id }) => {
|
||||||
|
cpu::set_max_frequency(freq_mhz, core_id).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
|
||||||
|
}
|
||||||
|
Some(Commands::SetPlatformProfile { profile }) => {
|
||||||
|
cpu::set_platform_profile(&profile).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
println!("Welcome to superfreq! Use --help for commands.");
|
||||||
|
println!("Current effective configuration: {:?}", config);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = command_result {
|
||||||
|
eprintln!("Error executing command: {}", e);
|
||||||
|
if let Some(source) = e.source() {
|
||||||
|
eprintln!("Caused by: {}", source);
|
||||||
|
}
|
||||||
|
// TODO: Consider specific error handling for PermissionDenied from cpu here
|
||||||
|
// For example, check if e.downcast_ref::<cpu::ControlError>() matches PermissionDenied
|
||||||
|
// and print a more specific message like "Try running with sudo."
|
||||||
|
// We'll revisit this in the future once CPU logic is more stable.
|
||||||
|
if let Some(control_error) = e.downcast_ref::<cpu::ControlError>() {
|
||||||
|
if matches!(control_error, cpu::ControlError::PermissionDenied(_)) {
|
||||||
|
eprintln!("Hint: This operation may require administrator privileges (e.g., run with sudo).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
364
src/monitor.rs
Normal file
364
src/monitor.rs
Normal file
|
@ -0,0 +1,364 @@
|
||||||
|
use crate::core::{SystemInfo, CpuCoreInfo, CpuGlobalInfo, BatteryInfo, SystemLoad, SystemReport};
|
||||||
|
use crate::config::AppConfig;
|
||||||
|
use std::{fs, io, path::{Path, PathBuf}, str::FromStr, time::SystemTime};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum SysMonitorError {
|
||||||
|
Io(io::Error),
|
||||||
|
ReadError(String),
|
||||||
|
ParseError(String),
|
||||||
|
NotAvailable(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<io::Error> for SysMonitorError {
|
||||||
|
fn from(err: io::Error) -> SysMonitorError {
|
||||||
|
SysMonitorError::Io(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for SysMonitorError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
SysMonitorError::Io(e) => write!(f, "I/O error: {}", e),
|
||||||
|
SysMonitorError::ReadError(s) => write!(f, "Failed to read sysfs path: {}", s),
|
||||||
|
SysMonitorError::ParseError(s) => write!(f, "Failed to parse value: {}", s),
|
||||||
|
SysMonitorError::NotAvailable(s) => write!(f, "Information not available: {}", s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for SysMonitorError {}
|
||||||
|
|
||||||
|
pub type Result<T, E = SysMonitorError> = std::result::Result<T, E>;
|
||||||
|
|
||||||
|
// Read a sysfs file to a string, trimming whitespace
|
||||||
|
fn read_sysfs_file_trimmed(path: impl AsRef<Path>) -> Result<String> {
|
||||||
|
fs::read_to_string(path.as_ref())
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.map_err(|e| {
|
||||||
|
SysMonitorError::ReadError(format!("Path: {:?}, Error: {}", path.as_ref().display(), e))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read a sysfs file and parse it to a specific type
|
||||||
|
fn read_sysfs_value<T: FromStr>(path: impl AsRef<Path>) -> Result<T> {
|
||||||
|
let content = read_sysfs_file_trimmed(path.as_ref())?;
|
||||||
|
content.parse::<T>().map_err(|_| {
|
||||||
|
SysMonitorError::ParseError(format!(
|
||||||
|
"Could not parse '{}' from {:?}",
|
||||||
|
content,
|
||||||
|
path.as_ref().display()
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_system_info() -> Result<SystemInfo> {
|
||||||
|
let mut cpu_model = "Unknown".to_string();
|
||||||
|
if let Ok(cpuinfo) = fs::read_to_string("/proc/cpuinfo") {
|
||||||
|
for line in cpuinfo.lines() {
|
||||||
|
if line.starts_with("model name") {
|
||||||
|
if let Some(val) = line.split(':').nth(1) {
|
||||||
|
cpu_model = val.trim().to_string();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let architecture = std::env::consts::ARCH.to_string();
|
||||||
|
|
||||||
|
let mut linux_distribution = "Unknown".to_string();
|
||||||
|
if let Ok(os_release) = fs::read_to_string("/etc/os-release") {
|
||||||
|
for line in os_release.lines() {
|
||||||
|
if line.starts_with("PRETTY_NAME=") {
|
||||||
|
if let Some(val) = line.split('=').nth(1) {
|
||||||
|
linux_distribution = val.trim_matches('"').to_string();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Ok(lsb_release) = fs::read_to_string("/etc/lsb-release") { // fallback for some systems
|
||||||
|
for line in lsb_release.lines() {
|
||||||
|
if line.starts_with("DISTRIB_DESCRIPTION=") {
|
||||||
|
if let Some(val) = line.split('=').nth(1) {
|
||||||
|
linux_distribution = val.trim_matches('"').to_string();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Ok(SystemInfo {
|
||||||
|
cpu_model,
|
||||||
|
architecture,
|
||||||
|
linux_distribution,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_logical_core_count() -> Result<u32> {
|
||||||
|
let mut count = 0;
|
||||||
|
let path = Path::new("/sys/devices/system/cpu");
|
||||||
|
if path.exists() {
|
||||||
|
for entry in fs::read_dir(path)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let name = entry.file_name();
|
||||||
|
if let Some(name_str) = name.to_str() {
|
||||||
|
if name_str.starts_with("cpu") &&
|
||||||
|
name_str.len() > 3 &&
|
||||||
|
name_str[3..].chars().all(char::is_numeric) {
|
||||||
|
// Check if it's a directory representing a core that can have cpufreq
|
||||||
|
if entry.path().join("cpufreq").exists() {
|
||||||
|
count += 1;
|
||||||
|
} else if Path::new(&format!("/sys/devices/system/cpu/{}/online", name_str)).exists() {
|
||||||
|
// Fallback for cores that might not have cpufreq but are online (e.g. E-cores on some setups before driver loads)
|
||||||
|
// This is a simplification; true cpufreq capability is key.
|
||||||
|
// If cpufreq dir doesn't exist, it might not be controllable by this tool.
|
||||||
|
// For counting purposes, we count it if it's an online CPU.
|
||||||
|
count +=1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
// Fallback to num_cpus crate if sysfs parsing fails or yields 0
|
||||||
|
Ok(num_cpus::get() as u32)
|
||||||
|
} else {
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn get_cpu_core_info(core_id: u32) -> Result<CpuCoreInfo> {
|
||||||
|
let cpufreq_path = PathBuf::from(format!("/sys/devices/system/cpu/cpu{}/cpufreq/", core_id));
|
||||||
|
|
||||||
|
let current_frequency_mhz = read_sysfs_value::<u32>(cpufreq_path.join("scaling_cur_freq"))
|
||||||
|
.map(|khz| khz / 1000)
|
||||||
|
.ok();
|
||||||
|
let min_frequency_mhz = read_sysfs_value::<u32>(cpufreq_path.join("scaling_min_freq"))
|
||||||
|
.map(|khz| khz / 1000)
|
||||||
|
.ok();
|
||||||
|
let max_frequency_mhz = read_sysfs_value::<u32>(cpufreq_path.join("scaling_max_freq"))
|
||||||
|
.map(|khz| khz / 1000)
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
// Temperature: Iterate through hwmon to find core-specific temperatures
|
||||||
|
// This is a common but not universal approach.
|
||||||
|
let mut temperature_celsius: Option<f32> = None;
|
||||||
|
if let Ok(hwmon_dir) = fs::read_dir("/sys/class/hwmon") {
|
||||||
|
for hw_entry in hwmon_dir.flatten() {
|
||||||
|
let hw_path = hw_entry.path();
|
||||||
|
// Try to find a label that indicates it's for this core or package
|
||||||
|
// e.g. /sys/class/hwmon/hwmonX/name might be "coretemp" or similar
|
||||||
|
// and /sys/class/hwmon/hwmonX/tempY_label might be "Core Z" or "Physical id 0"
|
||||||
|
// This is highly system-dependent, and not all systems will have this. For now,
|
||||||
|
// we'll try a common pattern for "coretemp" driver because it works:tm: on my system.
|
||||||
|
if let Ok(name) = read_sysfs_file_trimmed(hw_path.join("name")) {
|
||||||
|
if name == "coretemp" { // Common driver for Intel core temperatures
|
||||||
|
for i in 1..=16 { // Check a reasonable number of temp inputs
|
||||||
|
let label_path = hw_path.join(format!("temp{}_label", i));
|
||||||
|
let input_path = hw_path.join(format!("temp{}_input", i));
|
||||||
|
if label_path.exists() && input_path.exists() {
|
||||||
|
if let Ok(label) = read_sysfs_file_trimmed(&label_path) {
|
||||||
|
// Example: "Core 0", "Core 1", etc. or "Physical id 0" for package
|
||||||
|
if label.eq_ignore_ascii_case(&format!("Core {}", core_id)) ||
|
||||||
|
label.eq_ignore_ascii_case(&format!("Package id {}", core_id)) { //core_id might map to package for some sensors
|
||||||
|
if let Ok(temp_mc) = read_sysfs_value::<i32>(&input_path) {
|
||||||
|
temperature_celsius = Some(temp_mc as f32 / 1000.0);
|
||||||
|
break; // found temp for this core
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if temperature_celsius.is_some() { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: This is a placeholder so that I can actually run the code. It is a little
|
||||||
|
//complex to calculate from raw sysfs/procfs data. It typically involves reading /proc/stat
|
||||||
|
// and calculating deltas over time. This is out of scope for simple sysfs reads here.
|
||||||
|
// We will be returning here to this later.
|
||||||
|
let usage_percent: Option<f32> = None;
|
||||||
|
|
||||||
|
Ok(CpuCoreInfo {
|
||||||
|
core_id,
|
||||||
|
current_frequency_mhz,
|
||||||
|
min_frequency_mhz,
|
||||||
|
max_frequency_mhz,
|
||||||
|
usage_percent,
|
||||||
|
temperature_celsius,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all_cpu_core_info() -> Result<Vec<CpuCoreInfo>> {
|
||||||
|
let num_cores = get_logical_core_count()?;
|
||||||
|
(0..num_cores).map(get_cpu_core_info).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_cpu_global_info() -> Result<CpuGlobalInfo> {
|
||||||
|
// FIXME: Assume global settings can be read from cpu0 or are consistent.
|
||||||
|
// This might not work properly for heterogeneous systems (e.g. big.LITTLE)
|
||||||
|
let cpufreq_base = Path::new("/sys/devices/system/cpu/cpu0/cpufreq/");
|
||||||
|
|
||||||
|
let current_governor = if cpufreq_base.join("scaling_governor").exists() {
|
||||||
|
read_sysfs_file_trimmed(cpufreq_base.join("scaling_governor")).ok()
|
||||||
|
} else { None };
|
||||||
|
|
||||||
|
let available_governors = if cpufreq_base.join("scaling_available_governors").exists() {
|
||||||
|
read_sysfs_file_trimmed(cpufreq_base.join("scaling_available_governors"))
|
||||||
|
.map(|s| s.split_whitespace().map(String::from).collect())
|
||||||
|
.unwrap_or_else(|_| vec![])
|
||||||
|
} else { vec![] };
|
||||||
|
|
||||||
|
let turbo_status = if Path::new("/sys/devices/system/cpu/intel_pstate/no_turbo").exists() {
|
||||||
|
// 0 means turbo enabled, 1 means disabled for intel_pstate
|
||||||
|
read_sysfs_value::<u8>("/sys/devices/system/cpu/intel_pstate/no_turbo").map(|val| val == 0).ok()
|
||||||
|
} else if Path::new("/sys/devices/system/cpu/cpufreq/boost").exists() {
|
||||||
|
// 1 means turbo enabled, 0 means disabled for generic cpufreq boost
|
||||||
|
read_sysfs_value::<u8>("/sys/devices/system/cpu/cpufreq/boost").map(|val| val == 1).ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let epp = read_sysfs_file_trimmed(cpufreq_base.join("energy_performance_preference")).ok();
|
||||||
|
|
||||||
|
// EPB is often an integer 0-15. Reading as string for now.
|
||||||
|
let epb = read_sysfs_file_trimmed(cpufreq_base.join("energy_performance_bias")).ok();
|
||||||
|
|
||||||
|
let platform_profile = read_sysfs_file_trimmed("/sys/firmware/acpi/platform_profile").ok();
|
||||||
|
let _platform_profile_choices = read_sysfs_file_trimmed("/sys/firmware/acpi/platform_profile_choices").ok();
|
||||||
|
|
||||||
|
|
||||||
|
Ok(CpuGlobalInfo {
|
||||||
|
current_governor,
|
||||||
|
available_governors,
|
||||||
|
turbo_status,
|
||||||
|
epp,
|
||||||
|
epb,
|
||||||
|
platform_profile,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_battery_info(config: &AppConfig) -> Result<Vec<BatteryInfo>> {
|
||||||
|
let mut batteries = Vec::new();
|
||||||
|
let power_supply_path = Path::new("/sys/class/power_supply");
|
||||||
|
|
||||||
|
if !power_supply_path.exists() {
|
||||||
|
return Ok(batteries); // no power supply directory
|
||||||
|
}
|
||||||
|
|
||||||
|
let ignored_supplies = config.ignored_power_supplies.as_ref().cloned().unwrap_or_default();
|
||||||
|
|
||||||
|
// Determine overall AC connection status
|
||||||
|
let mut overall_ac_connected = false;
|
||||||
|
for entry in fs::read_dir(power_supply_path)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let ps_path = entry.path();
|
||||||
|
let name = entry.file_name().into_string().unwrap_or_default();
|
||||||
|
|
||||||
|
// Check for AC adapter type (common names: AC, ACAD, ADP)
|
||||||
|
if let Ok(ps_type) = read_sysfs_file_trimmed(ps_path.join("type")) {
|
||||||
|
if ps_type == "Mains" || ps_type == "USB_PD_DRP" || ps_type == "USB_PD" || ps_type == "USB_DCP" || ps_type == "USB_CDP" || ps_type == "USB_ACA" { // USB types can also provide power
|
||||||
|
if let Ok(online) = read_sysfs_value::<u8>(ps_path.join("online")) {
|
||||||
|
if online == 1 {
|
||||||
|
overall_ac_connected = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if name.starts_with("AC") || name.contains("ACAD") || name.contains("ADP") { // fallback for type file missing
|
||||||
|
if let Ok(online) = read_sysfs_value::<u8>(ps_path.join("online")) {
|
||||||
|
if online == 1 {
|
||||||
|
overall_ac_connected = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for entry in fs::read_dir(power_supply_path)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let ps_path = entry.path();
|
||||||
|
let name = entry.file_name().into_string().unwrap_or_default();
|
||||||
|
|
||||||
|
if ignored_supplies.contains(&name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(ps_type) = read_sysfs_file_trimmed(ps_path.join("type")) {
|
||||||
|
if ps_type == "Battery" {
|
||||||
|
let status_str = read_sysfs_file_trimmed(ps_path.join("status")).ok();
|
||||||
|
let capacity_percent = read_sysfs_value::<u8>(ps_path.join("capacity")).ok();
|
||||||
|
|
||||||
|
let power_rate_watts = if ps_path.join("power_now").exists() {
|
||||||
|
read_sysfs_value::<i32>(ps_path.join("power_now")) // uW
|
||||||
|
.map(|uw| uw as f32 / 1_000_000.0)
|
||||||
|
.ok()
|
||||||
|
} else if ps_path.join("current_now").exists() && ps_path.join("voltage_now").exists() {
|
||||||
|
let current_ua = read_sysfs_value::<i32>(ps_path.join("current_now")).ok(); // uA
|
||||||
|
let voltage_uv = read_sysfs_value::<i32>(ps_path.join("voltage_now")).ok(); // uV
|
||||||
|
if let (Some(c), Some(v)) = (current_ua, voltage_uv) {
|
||||||
|
// Power (W) = (Voltage (V) * Current (A))
|
||||||
|
// (v / 1e6 V) * (c / 1e6 A) = (v * c / 1e12) W
|
||||||
|
Some((c as f64 * v as f64 / 1_000_000_000_000.0) as f32)
|
||||||
|
} else { None }
|
||||||
|
} else { None };
|
||||||
|
|
||||||
|
let charge_start_threshold = read_sysfs_value::<u8>(ps_path.join("charge_control_start_threshold")).ok();
|
||||||
|
let charge_stop_threshold = read_sysfs_value::<u8>(ps_path.join("charge_control_end_threshold")).ok();
|
||||||
|
|
||||||
|
batteries.push(BatteryInfo {
|
||||||
|
name: name.clone(),
|
||||||
|
ac_connected: overall_ac_connected,
|
||||||
|
charging_state: status_str,
|
||||||
|
capacity_percent,
|
||||||
|
power_rate_watts,
|
||||||
|
charge_start_threshold,
|
||||||
|
charge_stop_threshold,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(batteries)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_system_load() -> Result<SystemLoad> {
|
||||||
|
let loadavg_str = read_sysfs_file_trimmed("/proc/loadavg")?;
|
||||||
|
let parts: Vec<&str> = loadavg_str.split_whitespace().collect();
|
||||||
|
if parts.len() < 3 {
|
||||||
|
return Err(SysMonitorError::ParseError(
|
||||||
|
"Could not parse /proc/loadavg: expected at least 3 parts".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let load_avg_1min = parts[0].parse().map_err(|_| SysMonitorError::ParseError(format!("Failed to parse 1min load: {}", parts[0])))?;
|
||||||
|
let load_avg_5min = parts[1].parse().map_err(|_| SysMonitorError::ParseError(format!("Failed to parse 5min load: {}", parts[1])))?;
|
||||||
|
let load_avg_15min = parts[2].parse().map_err(|_| SysMonitorError::ParseError(format!("Failed to parse 15min load: {}", parts[2])))?;
|
||||||
|
|
||||||
|
Ok(SystemLoad {
|
||||||
|
load_avg_1min,
|
||||||
|
load_avg_5min,
|
||||||
|
load_avg_15min,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collect_system_report(config: &AppConfig) -> Result<SystemReport> {
|
||||||
|
let system_info = get_system_info()?;
|
||||||
|
let cpu_cores = get_all_cpu_core_info()?;
|
||||||
|
let cpu_global = get_cpu_global_info()?;
|
||||||
|
let batteries = get_battery_info(config)?;
|
||||||
|
let system_load = get_system_load()?;
|
||||||
|
|
||||||
|
Ok(SystemReport {
|
||||||
|
system_info,
|
||||||
|
cpu_cores,
|
||||||
|
cpu_global,
|
||||||
|
batteries,
|
||||||
|
system_load,
|
||||||
|
timestamp: SystemTime::now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue