Compare commits

..

No commits in common. "main" and "v2.0.0" have entirely different histories.
main ... v2.0.0

42 changed files with 170 additions and 903 deletions

View file

@ -20,7 +20,7 @@ body:
id: reproduction id: reproduction
attributes: attributes:
label: Steps To Reproduce label: Steps To Reproduce
description: Steps to reproduce the behaviour. description: Steps to reproduce the behavior
placeholder: | placeholder: |
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
@ -33,12 +33,12 @@ body:
attributes: attributes:
label: Anything else? label: Anything else?
description: | description: |
Screenshots? Logs? limits_overrides.ron? limits_cache.ron? Anything that will give more context about the problem! Screenshots? Logs? pt_oc.json? limits_overrides.json? Anything that will give more context about the problem!
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
Please include the log (located at `/tmp/powertools.log`) if possible. Please include the log (located at `/tmp/powertools.log`) if possible.
Note: the log is deleted when the device is restarted. Note: the log is deleted when the device is restarted.
Tip: You can attach files by clicking this area to highlight it and then dragging them in. Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations: validations:
required: false required: false
- type: input - type: input

View file

@ -1,5 +1,5 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: NGnius - name: NGnius
url: https://git.ngni.us/sys/website/wiki#contact-information url: https://github.com/NGnius
about: Repository owner about: Repository owner

View file

@ -3,7 +3,7 @@ description: Suggest functionality to add
labels: "enhancement" labels: "enhancement"
body: body:
- type: textarea - type: textarea
id: feature-description id: described
attributes: attributes:
label: Describe what you'd like to be able to do label: Describe what you'd like to be able to do
description: A clear and concise description of what you want. description: A clear and concise description of what you want.
@ -22,11 +22,7 @@ body:
id: extras id: extras
attributes: attributes:
label: Anything else? label: Anything else?
description: | description: Description of how this can be achieved, or other additional context
Description of how this can be achieved, or other additional context.
If this is related to the UI, consider adding a picture.
Tip: You can attach files by clicking this area to highlight it and then dragging them in.
placeholder: This can be accomplished by doing ... placeholder: This can be accomplished by doing ...
validations: validations:
required: false required: false

View file

@ -1,23 +0,0 @@
name: "Question"
description: "Ask for more information about PowerTools"
labels: "question"
body:
- type: textarea
id: question-elaboration
attributes:
label: Question
description: |
A clear and concise description of what you'd like to know.
Please check the wiki and closed issues to avoid waiting for an answer when you didn't need to.
validations:
required: true
- type: textarea
id: extras
attributes:
label: Extra Info
description: |
Additional context or information which may be helpful when answering your question.
Tip: You can attach files by clicking this area to highlight it and then dragging them in.
validations:
required: false

View file

@ -1,13 +0,0 @@
## Description
A short description of what the PR changes.
If this is a minor change and the commit message(s) explains it well, feel free to delete this section.
## Motivation
Why did you write this PR and/or why should it be accepted.
## Fixes
Please indicate issues that this fixes or addresses here.
If this is a bugfix with no pre-existing issue, please describe the bug here instead.

1
.gitignore vendored
View file

@ -49,4 +49,3 @@ yalc.lock
# packaging # packaging
/PowerTools /PowerTools
**.zip **.zip
deck.json

View file

@ -1,12 +1,12 @@
# PowerTools # PowerTools
<!-- TODO Update badges for new git repo location --> <!-- TODO Update badges for new git repo location -->
[![Decky store](https://img.shields.io/badge/dynamic/json?color=blue&label=release&query=%24%5B%3F%28%40.name%3D%3D%27PowerTools%27%29%5D.versions%5B0%5D.name&url=https%3A%2F%2Fplugins.deckbrew.xyz%2Fplugins&style=flat-square)](https://plugins.deckbrew.xyz/) [![Decky store](https://img.shields.io/badge/dynamic/json?color=blue&label=release&query=%24%5B%3F%28%40.name%3D%3D%27PowerTools%27%29%5D.versions%5B0%5D.name&url=https%3A%2F%2Fplugins.deckbrew.xyz%2Fplugins&style=flat-square)](https://plugins.deckbrew.xyz/)
[![Custom store](https://img.shields.io/badge/dynamic/json?color=blue&label=preview&query=%24%5B%3F%28%40.name%3D%3D%27PowerTools%27%29%5D.versions%5B0%5D.name&url=https%3A%2F%2Fnot-decky-alpha.ngni.us%2Fplugins&style=flat-square)](https://git.ngni.us/NG-SD-Plugins/PowerTools/wiki) [![Custom store](https://img.shields.io/badge/dynamic/json?color=blue&label=preview&query=%24%5B%3F%28%40.name%3D%3D%27PowerTools%27%29%5D.versions%5B0%5D.name&url=https%3A%2F%2Fnot-decky-alpha.ngni.us%2Fplugins&style=flat-square)](https://github.com/NGnius/PowerTools/wiki)
[![GitHub package.json version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgit.ngni.us%2FNG-SD-Plugins%2FPowerTools%2Fraw%2Fbranch%2Fmain%2Fpackage.json&query=%24.version&style=flat-square&label=local&cacheSeconds=600)](https://git.ngni.us/NG-SD-Plugins/PowerTools/src/branch/main/package.json) [![GitHub package.json version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgit.ngni.us%2FNG-SD-Plugins%2FPowerTools%2Fraw%2Fbranch%2Fmain%2Fpackage.json&query=%24.version&style=flat-square&label=local&cacheSeconds=600)](https://git.ngni.us/NG-SD-Plugins/PowerTools/src/branch/main/package.json)
[![Liberapay](https://img.shields.io/liberapay/patrons/NGnius?style=flat-square)](https://liberapay.com/NGnius) [![Liberapay](https://img.shields.io/liberapay/patrons/NGnius?style=flat-square)](https://liberapay.com/NGnius)
[![GitHub](https://img.shields.io/badge/GPL--3.0-orange?style=flat-square&label=license&cacheSeconds=600)](https://git.ngni.us/NG-SD-Plugins/PowerTools/blob/main/LICENSE) [![GitHub](https://img.shields.io/badge/GPL--3.0-orange?style=flat-square&label=license&cacheSeconds=600)](https://github.com/NGnius/PowerTools/blob/main/LICENSE)
[![GitHub package.json dependency version (local)](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgit.ngni.us%2FNG-SD-Plugins%2FPowerTools%2Fraw%2Fbranch%2Fmain%2Fpackage.json&query=%24..%5B'decky-frontend-lib'%5D&style=flat-square&label=decky-frontend-lib&cacheSeconds=600)](https://git.ngni.us/NG-SD-Plugins/PowerTools/blob/main/pnpm-lock.yaml) [![GitHub package.json dependency version (local)](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgit.ngni.us%2FNG-SD-Plugins%2FPowerTools%2Fraw%2Fbranch%2Fmain%2Fpackage.json&query=%24..%5B'decky-frontend-lib'%5D&style=flat-square&label=decky-frontend-lib&cacheSeconds=600)](https://github.com/NGnius/PowerTools/blob/main/pnpm-lock.yaml)
![plugin_demo](./assets/ui.png) ![plugin_demo](./assets/ui.png)
@ -20,38 +20,24 @@ You will need that installed for this plugin to work.
- Enable & disable CPU threads & SMT - Enable & disable CPU threads & SMT
- Set CPU frequencies - Set CPU frequencies
- Set GPU frequencies and power (fastPPT & slowPPT) - Set GPU frequencies and power (fastPPT & slowPPT)
- Cap battery charge level - Cap battery charge rate (when awake)
- Display supplementary battery info - Display supplementary battery info
- Keep settings between restarts (stored in `~/homebrew/settings/PowerTools/<appId>.ron`) - Keep settings between restarts (stored in `~/.config/powertools/<gameId>.json`)
This plugin is tested on Steam Deck LCD/OLED, but is designed to work on other Linux devices as well. Unfortunately I am currently unable to test on most other devices. This plugin is tested on Steam Deck, but is designed to work on other Linux devices as well. Unfortunately I am currently unable to test on other devices.
## Install ## Install
Please use Decky's [built-in store](https://plugins.deckbrew.xyz/) to install official releases. Please use Decky's [built-in store](https://plugins.deckbrew.xyz/) to install official releases.
If you want to test unstable versions, use [my custom store](https://not-decky-alpha.ngni.us/plugins). If you would like to use an in-development version, feel free to build PowerTools yourself. If you want to test unstable versions, use [my custom store](https://not-decky-alpha.ngni.us/plugins). If you would like to use an in-development version, feel free to build PowerTools yourself.
## Build/Deploy ## Build
0. Requirements: a functioning Rust toolchain for x86_64-unknown-linux-gnu (or -musl), pnpm, and some tech literacy 0. Requirements: a functioning Rust toolchain for x86_64-unknown-linux-gnu (or -musl), pnpm, and some tech literacy
1. In a terminal, navigate to the backend directory of this project and run `./build.sh` 1. In a terminal, navigate to the backend directory of this project and run `./build.sh`
2. In the root of this project, run `pnpm run build` 2. In the root of this project, run `pnpm run build`
3. Transfer the project (especially dist/ and bin/) to a folder in your Steam Deck's `~/homebrew/plugins` directory 3. Transfer the project (especially dist/ and bin/) to a folder in your Steam Deck's `~/homebrew/plugins` directory
4. Restart Decky with `sudo systemctl restart plugin_loader.service`
## License ## License
This is licensed under GNU GPLv3. This is licensed under GNU GPLv3.
## Contributing
All contributions are welcome!
Anything from a comment on an issue to a new feature pull request will be appreciated by PowerTools's crack team of one (NGnius).
### Translations
Adding new languages and keeping existing language files up to date makes PowerTools more accessible to the majority of the world which doesn't speak English. Take a look at [this comment](https://git.ngni.us/NG-SD-Plugins/PowerTools/issues/9#issuecomment-345) (and the rest of that issue) to get started.
### Code
To prevent spam, this server does not allow regular users to create/fork repositories. Please open an issue [here](https://git.ngni.us/sys/website) to request permission. There's no pressure to actually do anything with that permission, though it may be revoked when the server is running low on space.

10
backend/Cargo.lock generated
View file

@ -1170,7 +1170,7 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "powertools" name = "powertools"
version = "2.0.3" version = "2.0.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"chrono", "chrono",
@ -1481,9 +1481,9 @@ dependencies = [
[[package]] [[package]]
name = "smokepatio" name = "smokepatio"
version = "0.2.0" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3416e8c907d171c4334df3933873c32bff97ca5ad7ae0ee93e6268e04e2041ef" checksum = "626ef8beee78bebc397d841469fa47bf7e370ddb8b8f3e628e69b03bf968d089"
dependencies = [ dependencies = [
"embedded-io", "embedded-io",
"log", "log",
@ -1540,9 +1540,9 @@ dependencies = [
[[package]] [[package]]
name = "sysfuss" name = "sysfuss"
version = "0.3.0" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f33bae529511a671b5f2ed4cc46ae0b2ccdf8c03ccf7eebe95a5a886ff7914dc" checksum = "4fa4dd5879b3fd41aff63991a59970cdfeced6f0d5920c5da0937279904d9f45"
[[package]] [[package]]
name = "termcolor" name = "termcolor"

View file

@ -1,6 +1,6 @@
[package] [package]
name = "powertools" name = "powertools"
version = "2.0.3" version = "2.0.0"
edition = "2021" edition = "2021"
authors = ["NGnius (Graham) <ngniusness@gmail.com>"] authors = ["NGnius (Graham) <ngniusness@gmail.com>"]
description = "Backend (superuser) functionality for PowerTools" description = "Backend (superuser) functionality for PowerTools"
@ -16,7 +16,7 @@ usdpl-back = { version = "0.10.1", features = ["blocking", "decky"] }#, path = "
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
ron = "0.8" ron = "0.8"
sysfuss = { version = "0.3", features = ["derive"] }#,path = "../../sysfs-nav"} sysfuss = { version = "0.2", features = ["derive"] }#,path = "../../sysfs-nav"}
# async # async
tokio = { version = "*", features = ["time"] } tokio = { version = "*", features = ["time"] }
@ -31,7 +31,7 @@ limits_core = { version = "3", path = "./limits_core" }
regex = "1" regex = "1"
# steam deck libs # steam deck libs
smokepatio = { version = "0.2", default-features = false } smokepatio = { version = "0.1", features = [ "std" ] }
libc = "0.2" libc = "0.2"
# online settings # online settings

View file

@ -1,34 +1,27 @@
#!/bin/bash #!/bin/bash
cd /backend echo "--- Rust version info ---"
echo "--- Debug info ---"
rustup --version rustup --version
rustc --version rustc --version
cargo --version cargo --version
id $USER mkdir -p out
stat /backend || exit 1
stat /backend/out || exit 1
echo $(uname -a) > /backend/out/version.txt
echo "--- Building ryzenadj lib ---" echo "--- Building ryzenadj lib ---"
git clone https://github.com/FlyGoat/RyzenAdj /tmp/ryzenadj git clone https://github.com/FlyGoat/RyzenAdj ryzenadj
cd /tmp/ryzenadj cd ryzenadj
git checkout -q v0.14.0 git checkout -q 160502771054d31d2f4c2fa46ad42c96336f3a74
mkdir build && cd build mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Release .. || exit 1 cmake -DCMAKE_BUILD_TYPE=Release ..
make || exit 1 make
mv libryzenadj.so /backend/out/libryzenadj.so || exit 1 mv libryzenadj.so ../../out/libryzenadj.so
mv ryzenadj /backend/out/ryzenadj || exit 1 cd ../..
cd /backend
echo "--- Building plugin backend ---" echo "--- Building plugin backend ---"
cargo build --profile docker || exit 1 cargo build --profile docker
mkdir -p out || exit 1 mkdir -p out
mv target/docker/powertools out/backend || exit 1 mv target/docker/powertools out/backend
echo " --- Cleaning up ---" echo " --- Cleaning up ---"
# remove root-owned target folder # remove root-owned target folder

View file

@ -3,11 +3,11 @@
#cargo build --release --target x86_64-unknown-linux-musl #cargo build --release --target x86_64-unknown-linux-musl
#cargo build --target x86_64-unknown-linux-musl #cargo build --target x86_64-unknown-linux-musl
#cross build #cross build
#cargo build --release cargo build --release
cargo build #cargo build
mkdir -p ../bin mkdir -p ../bin
#cp --preserve=mode ./target/x86_64-unknown-linux-musl/release/powertools ../bin/backend #cp --preserve=mode ./target/x86_64-unknown-linux-musl/release/powertools ../bin/backend
#cp --preserve=mode ./target/x86_64-unknown-linux-musl/debug/powertools ../bin/backend #cp --preserve=mode ./target/x86_64-unknown-linux-musl/debug/powertools ../bin/backend
#cp --preserve=mode ./target/release/powertools ../bin/backend cp --preserve=mode ./target/release/powertools ../bin/backend
cp --preserve=mode ./target/debug/powertools ../bin/backend #cp --preserve=mode ./target/debug/powertools ../bin/backend

View file

@ -438,7 +438,7 @@ dependencies = [
[[package]] [[package]]
name = "community_settings_srv" name = "community_settings_srv"
version = "0.1.1" version = "0.1.0"
dependencies = [ dependencies = [
"actix-web", "actix-web",
"clap", "clap",
@ -450,7 +450,6 @@ dependencies = [
"serde_json", "serde_json",
"simplelog", "simplelog",
"tokio", "tokio",
"walkdir",
] ]
[[package]] [[package]]
@ -1018,15 +1017,6 @@ version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@ -1329,16 +1319,6 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"

View file

@ -1,6 +1,6 @@
[package] [package]
name = "community_settings_srv" name = "community_settings_srv"
version = "0.1.1" version = "0.1.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -16,8 +16,6 @@ tokio = { version = "1", features = ["full"] }
actix-web = { version = "4.4" } actix-web = { version = "4.4" }
mime = { version = "0.3.17" } mime = { version = "0.3.17" }
walkdir = "2"
# logging # logging
log = "0.4" log = "0.4"
simplelog = "0.12" simplelog = "0.12"

View file

@ -1,3 +0,0 @@
#!/bin/bash
cargo build --release --target aarch64-unknown-linux-musl

View file

@ -60,12 +60,11 @@ fn get_some_settings_by_app_id(steam_app_id: u32, cli: &'static Cli) -> std::io:
let app_id_folder = file_util::setting_folder_by_app_id(&cli.folder, steam_app_id); let app_id_folder = file_util::setting_folder_by_app_id(&cli.folder, steam_app_id);
let mut files: Vec<_> = app_id_folder.read_dir()? let mut files: Vec<_> = app_id_folder.read_dir()?
.filter_map(|res| res.ok()) .filter_map(|res| res.ok())
.filter(|f| f.path().extension().map(|ext| ext == crate::consts::RON_EXTENSION).unwrap_or(false)) .filter(|f| f.path().extension().map(|ext| ext == file_util::RON_EXTENSION).unwrap_or(false))
.filter_map(|f| f.metadata().ok().map(|meta| (f, meta))) .filter_map(|f| f.metadata().ok().map(|meta| (f, meta)))
.filter_map(|(f, meta)| meta.modified().ok().map(|time| (f, meta, time))) .filter_map(|(f, meta)| meta.created().ok().map(|time| (f, meta, time)))
.collect(); .collect();
files.sort_by(|(_, _, a_created), (_, _, b_created)| a_created.cmp(b_created)); files.sort_by(|(_, _, a_created), (_, _, b_created)| a_created.cmp(b_created));
let files_len = files.len();
let mut results = Vec::with_capacity(MAX_RESULTS); let mut results = Vec::with_capacity(MAX_RESULTS);
for (_, (f, _, _)) in files.into_iter().enumerate().take_while(|(i, _)| *i < MAX_RESULTS) { for (_, (f, _, _)) in files.into_iter().enumerate().take_while(|(i, _)| *i < MAX_RESULTS) {
@ -73,14 +72,12 @@ fn get_some_settings_by_app_id(steam_app_id: u32, cli: &'static Cli) -> std::io:
let setting = match ron::de::from_reader(reader) { let setting = match ron::de::from_reader(reader) {
Ok(x) => x, Ok(x) => x,
Err(e) => { Err(e) => {
log::debug!("Error while reading {}: {}", f.path().display(), e);
let e_msg = format!("{}", e); let e_msg = format!("{}", e);
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg)); return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg));
} }
}; };
results.push(setting); results.push(setting);
} }
log::debug!("Got {} results (from {} files) for {}", results.len(), files_len, app_id_folder.display());
Ok(results) Ok(results)
} }
@ -91,8 +88,7 @@ pub async fn get_setting_by_app_id_handler(
cli: web::Data<&'static Cli>, cli: web::Data<&'static Cli>,
) -> std::io::Result<impl Responder> { ) -> std::io::Result<impl Responder> {
let id: u32 = *id; let id: u32 = *id;
#[cfg(debug_assertions)] println!("Accept: {}", accept.to_string());
log::debug!("Accept: {}", accept.to_string());
let preferred = accept.preference(); let preferred = accept.preference();
if super::is_mime_type_ron_capable(&preferred) { if super::is_mime_type_ron_capable(&preferred) {
// Send RON // Send RON

View file

@ -67,7 +67,7 @@ pub async fn get_setting_handler(
if super::is_mime_type_ron_capable(&preferred) { if super::is_mime_type_ron_capable(&preferred) {
// Send RON // Send RON
let ron = if id != 0 { let ron = if id != 0 {
let path = file_util::setting_path_by_id(&cli.folder, id, crate::consts::RON_EXTENSION); let path = file_util::setting_path_by_id(&cli.folder, id, file_util::RON_EXTENSION);
if !path.exists() { if !path.exists() {
return Err(std::io::Error::new(std::io::ErrorKind::NotFound, format!("setting id {} does not exist", id))); return Err(std::io::Error::new(std::io::ErrorKind::NotFound, format!("setting id {} does not exist", id)));
} }
@ -93,7 +93,7 @@ pub async fn get_setting_handler(
} else { } else {
// Send JSON (fallback) // Send JSON (fallback)
let json = if id != 0 { let json = if id != 0 {
let path = file_util::setting_path_by_id(&cli.folder, id, crate::consts::JSON_EXTENSION); let path = file_util::setting_path_by_id(&cli.folder, id, file_util::JSON_EXTENSION);
// TODO? cache this instead of always loading it from file // TODO? cache this instead of always loading it from file
let reader = std::io::BufReader::new(std::fs::File::open(path)?); let reader = std::io::BufReader::new(std::fs::File::open(path)?);
match serde_json::from_reader(reader) { match serde_json::from_reader(reader) {

View file

@ -18,7 +18,7 @@ pub async fn save_setting_handler(
Err(_e) => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "too many bytes in payload")), Err(_e) => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "too many bytes in payload")),
}; };
let next_id = file_util::next_setting_id(&cli.folder); let next_id = file_util::next_setting_id(&cli.folder);
let mut parsed_data: community_settings_core::v1::Metadata = if super::is_mime_type_ron_capable(&content_type) { let parsed_data: community_settings_core::v1::Metadata = if super::is_mime_type_ron_capable(&content_type) {
// Parse as RON // Parse as RON
match ron::de::from_reader(bytes.as_ref()) { match ron::de::from_reader(bytes.as_ref()) {
Ok(x) => x, Ok(x) => x,
@ -42,18 +42,16 @@ pub async fn save_setting_handler(
} }
} }
}; };
// Override the ID with the one used by this server
parsed_data.id = next_id.to_string();
// TODO validate user and app id // TODO validate user and app id
// Reject blocked users and apps // Reject blocked users and apps
let path_ron = file_util::setting_path_by_id(&cli.folder, next_id, crate::consts::RON_EXTENSION); let path_ron = file_util::setting_path_by_id(&cli.folder, next_id, file_util::RON_EXTENSION);
let writer = std::io::BufWriter::new(std::fs::File::create(&path_ron)?); let writer = std::io::BufWriter::new(std::fs::File::create(&path_ron)?);
if let Err(e) = ron::ser::to_writer(writer, &parsed_data) { if let Err(e) = ron::ser::to_writer(writer, &parsed_data) {
let e_msg = format!("{}", e); let e_msg = format!("{}", e);
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg)); return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg));
} }
let path_json = file_util::setting_path_by_id(&cli.folder, next_id, crate::consts::JSON_EXTENSION); let path_json = file_util::setting_path_by_id(&cli.folder, next_id, file_util::JSON_EXTENSION);
let writer = std::io::BufWriter::new(std::fs::File::create(&path_json)?); let writer = std::io::BufWriter::new(std::fs::File::create(&path_json)?);
if let Err(e) = serde_json::to_writer(writer, &parsed_data) { if let Err(e) = serde_json::to_writer(writer, &parsed_data) {
let e_msg = format!("{}", e); let e_msg = format!("{}", e);
@ -64,32 +62,57 @@ pub async fn save_setting_handler(
} }
} }
log::debug!("Saved to {}, building symlinks", path_ron.display()); // create symlinks for other ways of looking up these settings files
let filename_ron = file_util::filename(next_id, file_util::RON_EXTENSION);
let filename_json = file_util::filename(next_id, file_util::JSON_EXTENSION);
let to_symlink = file_util::symlinks(&cli.folder, &parsed_data)?; // create symlinks to app id folder
let app_id_folder = file_util::setting_folder_by_app_id(&cli.folder, parsed_data.steam_app_id);
let path_ron_canon = path_ron.canonicalize()?; if !app_id_folder.exists() {
let path_json_canon = path_json.canonicalize()?; std::fs::create_dir(&app_id_folder)?;
for ron_link in to_symlink.ron {
log::debug!("Symlinking {} -> {}", ron_link.display(), path_ron_canon.display());
#[cfg(target_family = "windows")] // NOTE: windows support is untested and unmaintained
{
std::os::windows::fs::symlink_file(&path_ron_canon, &ron_link)?;
}
#[cfg(target_family = "unix")]
{
std::os::unix::fs::symlink(&path_ron_canon, &ron_link)?;
}
} }
for json_link in to_symlink.json { #[cfg(target_family = "windows")] // NOTE: windows support is untested and unmaintained
//log::debug!("Symlinking {} -> {}", json_link.display(), path_json_canon.display()); {
std::os::windows::fs::symlink_file(&path_ron, app_id_folder.join(&filename_ron))?;
std::os::windows::fs::symlink_file(&path_json, app_id_folder.join(&filename_json))?;
}
#[cfg(target_family = "unix")]
{
std::os::unix::fs::symlink(&path_ron, app_id_folder.join(&filename_ron))?;
std::os::unix::fs::symlink(&path_json, app_id_folder.join(&filename_json))?;
}
// create symlinks for user id folder
let user_id_folder = file_util::setting_folder_by_user_id(&cli.folder, parsed_data.steam_user_id);
if !user_id_folder.exists() {
std::fs::create_dir(&user_id_folder)?;
}
#[cfg(target_family = "windows")] // NOTE: windows support is untested and unmaintained
{
std::os::windows::fs::symlink_file(&path_ron, user_id_folder.join(&filename_ron))?;
std::os::windows::fs::symlink_file(&path_json, user_id_folder.join(&filename_json))?;
}
#[cfg(target_family = "unix")]
{
std::os::unix::fs::symlink(&path_ron, user_id_folder.join(&filename_ron))?;
std::os::unix::fs::symlink(&path_json, user_id_folder.join(&filename_json))?;
}
// create symlinks for each tag
for tag in parsed_data.tags.iter() {
let tag_folder = file_util::setting_folder_by_tag(&cli.folder, tag);
if !tag_folder.exists() {
std::fs::create_dir(&tag_folder)?;
}
#[cfg(target_family = "windows")] // NOTE: windows support is untested and unmaintained #[cfg(target_family = "windows")] // NOTE: windows support is untested and unmaintained
{ {
std::os::windows::fs::symlink_file(&path_json_canon, json_link)?; std::os::windows::fs::symlink_file(&path_ron, tag_folder.join(&filename_ron))?;
std::os::windows::fs::symlink_file(&path_json, tag_folder.join(&filename_json))?;
} }
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
{ {
std::os::unix::fs::symlink(&path_json_canon, json_link)?; std::os::unix::fs::symlink(&path_ron, tag_folder.join(&filename_ron))?;
std::os::unix::fs::symlink(&path_json, tag_folder.join(&filename_json))?;
} }
} }

View file

@ -14,14 +14,6 @@ pub struct Cli {
/// Log file location /// Log file location
#[arg(short, long, default_value = "/tmp/powertools_community_settings_srv.log")] #[arg(short, long, default_value = "/tmp/powertools_community_settings_srv.log")]
pub log: std::path::PathBuf, pub log: std::path::PathBuf,
/// Perform maintenance tasks
#[arg(long)]
pub fix: bool,
/// Keep up to this many duplicate settings (0 will delete everything!!!)
#[arg(long, default_value_t = 2)]
pub duplicates: usize,
} }
impl Cli { impl Cli {

View file

@ -1,8 +0,0 @@
pub const RON_EXTENSION: &'static str = "ron";
pub const JSON_EXTENSION: &'static str = "json";
pub const SETTING_FOLDER: &'static str = "settings";
pub const ID_FOLDER: &'static str = "by_id";
pub const APP_ID_FOLDER: &'static str = "by_app_id";
pub const USER_ID_FOLDER: &'static str = "by_user_id";
pub const TAG_FOLDER: &'static str = "by_tag";

View file

@ -1,10 +1,41 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Mutex; use std::sync::Mutex;
use crate::consts::*; pub const RON_EXTENSION: &'static str = "ron";
pub const JSON_EXTENSION: &'static str = "json";
const SETTING_FOLDER: &'static str = "settings";
const ID_FOLDER: &'static str = "by_id";
const APP_ID_FOLDER: &'static str = "by_app_id";
const USER_ID_FOLDER: &'static str = "by_user_id";
const TAG_FOLDER: &'static str = "by_tag";
static LAST_SETTING_ID: Mutex<u128> = Mutex::new(0); static LAST_SETTING_ID: Mutex<u128> = Mutex::new(0);
pub fn build_folder_layout(root: impl AsRef<Path>) -> std::io::Result<()> {
std::fs::create_dir_all(
root.as_ref()
.join(SETTING_FOLDER)
.join(ID_FOLDER)
)?;
std::fs::create_dir_all(
root.as_ref()
.join(SETTING_FOLDER)
.join(APP_ID_FOLDER)
)?;
std::fs::create_dir_all(
root.as_ref()
.join(SETTING_FOLDER)
.join(USER_ID_FOLDER)
)?;
std::fs::create_dir_all(
root.as_ref()
.join(SETTING_FOLDER)
.join(TAG_FOLDER)
)?;
Ok(())
}
pub fn filename(id: u128, ext: &str) -> String { pub fn filename(id: u128, ext: &str) -> String {
format!("{}.{}", id, ext) format!("{}.{}", id, ext)
} }
@ -23,23 +54,6 @@ pub fn setting_folder_by_app_id(root: impl AsRef<Path>, steam_app_id: u32) -> Pa
.join(steam_app_id.to_string()) .join(steam_app_id.to_string())
} }
pub fn setting_tag_folder_by_app_id(root: impl AsRef<Path>, steam_app_id: u32) -> PathBuf {
root.as_ref()
.join(SETTING_FOLDER)
.join(APP_ID_FOLDER)
.join(steam_app_id.to_string())
.join(TAG_FOLDER)
}
pub fn setting_folder_by_app_id_tag(root: impl AsRef<Path>, steam_app_id: u32, tag: &str) -> PathBuf {
root.as_ref()
.join(SETTING_FOLDER)
.join(APP_ID_FOLDER)
.join(steam_app_id.to_string())
.join(TAG_FOLDER)
.join(tag)
}
pub fn setting_folder_by_user_id(root: impl AsRef<Path>, steam_user_id: u64) -> PathBuf { pub fn setting_folder_by_user_id(root: impl AsRef<Path>, steam_user_id: u64) -> PathBuf {
root.as_ref() root.as_ref()
.join(SETTING_FOLDER) .join(SETTING_FOLDER)
@ -47,23 +61,6 @@ pub fn setting_folder_by_user_id(root: impl AsRef<Path>, steam_user_id: u64) ->
.join(steam_user_id.to_string()) .join(steam_user_id.to_string())
} }
pub fn setting_tag_folder_by_user_id(root: impl AsRef<Path>, steam_user_id: u64) -> PathBuf {
root.as_ref()
.join(SETTING_FOLDER)
.join(USER_ID_FOLDER)
.join(steam_user_id.to_string())
.join(TAG_FOLDER)
}
pub fn setting_folder_by_user_id_tag(root: impl AsRef<Path>, steam_user_id: u64, tag: &str) -> PathBuf {
root.as_ref()
.join(SETTING_FOLDER)
.join(USER_ID_FOLDER)
.join(steam_user_id.to_string())
.join(TAG_FOLDER)
.join(tag)
}
pub fn setting_folder_by_tag(root: impl AsRef<Path>, tag: &str) -> PathBuf { pub fn setting_folder_by_tag(root: impl AsRef<Path>, tag: &str) -> PathBuf {
root.as_ref() root.as_ref()
.join(SETTING_FOLDER) .join(SETTING_FOLDER)
@ -76,83 +73,14 @@ pub fn next_setting_id(root: impl AsRef<Path>) -> u128 {
let mut last_id = *lock; let mut last_id = *lock;
if last_id == 0 { if last_id == 0 {
// needs init // needs init
last_id = 1;
let mut path = setting_path_by_id(root.as_ref(), last_id, RON_EXTENSION); let mut path = setting_path_by_id(root.as_ref(), last_id, RON_EXTENSION);
while path.exists() { while path.exists() {
last_id += 1; last_id += 1;
path = setting_path_by_id(root.as_ref(), last_id, RON_EXTENSION); path = setting_path_by_id(root.as_ref(), last_id, RON_EXTENSION);
} }
*lock = last_id - 1; *lock = last_id;
log::info!("setting id initialized to {}", last_id); println!("setting id initialized to {}", last_id);
} }
*lock += 1; *lock += 1;
*lock *lock
} }
pub struct ToSymlink {
pub ron: Vec<PathBuf>,
pub json: Vec<PathBuf>,
}
pub fn symlinks(root: impl AsRef<Path>, meta: &community_settings_core::v1::Metadata) -> std::io::Result<ToSymlink> {
let mut symlink_locations = ToSymlink { ron: Vec::new(), json: Vec::new() };
let filename_ron = filename(meta.get_id(), crate::consts::RON_EXTENSION);
let filename_json = filename(meta.get_id(), crate::consts::JSON_EXTENSION);
// build symlinks to app id folder
let app_id_folder = setting_folder_by_app_id(&root, meta.steam_app_id);
log::debug!("App id folder {}", app_id_folder.display());
if !app_id_folder.exists() {
std::fs::create_dir(&app_id_folder)?;
std::fs::create_dir(setting_tag_folder_by_app_id(&root, meta.steam_app_id))?;
}
symlink_locations.ron.push(app_id_folder.join(&filename_ron));
symlink_locations.json.push(app_id_folder.join(&filename_json));
// create symlinks for user id folder
let user_id_folder = setting_folder_by_user_id(&root, meta.steam_user_id);
if !user_id_folder.exists() {
std::fs::create_dir(&user_id_folder)?;
std::fs::create_dir(setting_tag_folder_by_user_id(&root, meta.steam_user_id))?;
}
symlink_locations.ron.push(user_id_folder.join(&filename_ron));
symlink_locations.json.push(user_id_folder.join(&filename_json));
// create symlinks for each tag
for tag in meta.tags.iter() {
if !str_is_alphanumeric_or_space(&tag){
continue;
}
// create symlinks for general tag folder
let tag_folder = setting_folder_by_tag(&root, tag);
if !tag_folder.exists() {
std::fs::create_dir(&tag_folder)?;
}
symlink_locations.ron.push(tag_folder.join(&filename_ron));
symlink_locations.json.push(tag_folder.join(&filename_json));
// create symlinks for app id tag folder
let app_tag_folder = setting_folder_by_app_id_tag(&root, meta.steam_app_id, tag);
if !app_tag_folder.exists() {
std::fs::create_dir(&app_tag_folder)?;
}
symlink_locations.ron.push(app_tag_folder.join(&filename_ron));
symlink_locations.json.push(app_tag_folder.join(&filename_json));
// create symlinks for user id tag folder
let user_tag_folder = setting_folder_by_user_id_tag(&root, meta.steam_user_id, tag);
if !user_tag_folder.exists() {
std::fs::create_dir(&user_tag_folder)?;
}
symlink_locations.ron.push(user_tag_folder.join(&filename_ron));
symlink_locations.json.push(user_tag_folder.join(&filename_json));
}
Ok(symlink_locations)
}
fn str_is_alphanumeric_or_space(s: &str) -> bool {
let mut result = true;
for ch in s.chars() {
result &= ch.is_ascii_alphanumeric() || ch == ' ';
}
result
}

View file

@ -1,9 +1,6 @@
mod api; mod api;
mod cli; mod cli;
mod consts;
mod file_util; mod file_util;
mod tasks;
mod upgrade;
use actix_web::{web, App, HttpServer}; use actix_web::{web, App, HttpServer};
@ -30,22 +27,7 @@ async fn main() -> std::io::Result<()> {
// setup // setup
log::debug!("Building folder layout (if not exists) at: {}", &args.folder.display()); log::debug!("Building folder layout (if not exists) at: {}", &args.folder.display());
upgrade::build_folder_layout(&args.folder)?; file_util::build_folder_layout(&args.folder)?;
// fix things
if args.fix {
log::info!("Fixing old symlinks");
upgrade::fix_symlinks(&args.folder)?;
log::info!("Creating missing by_tag folders");
upgrade::make_tag_subfolders(&args.folder)?;
log::info!("Resynchronizing file IDs with file name IDs");
upgrade::sync_ids(&args.folder)?;
log::info!("Rebuilding missing symlinks");
upgrade::rebuild_symlinks(&args.folder)?;
return Ok(())
}
tasks::start_tasks(args.clone());
let leaked_args: &'static cli::Cli = Box::leak::<'static>(Box::new(args)); let leaked_args: &'static cli::Cli = Box::leak::<'static>(Box::new(args));
HttpServer::new(move || { HttpServer::new(move || {

View file

@ -1,16 +0,0 @@
mod task_runner;
mod symlink_cleanup;
mod user_antispam;
pub fn start_tasks(args: crate::cli::Cli) {
task_runner::TaskRunner::new(
args.clone(),
symlink_cleanup::remove_broken_symlinks,
std::time::Duration::from_secs(1 * 60 /* 1 minute */),
).run();
task_runner::TaskRunner::new(
args.clone(),
user_antispam::remove_similar_user_uploads,
std::time::Duration::from_secs(5 * 60 /* 5 minutes */),
).run();
}

View file

@ -1,39 +0,0 @@
//! Realistically this shouldn't be occur unless there's corruption or a settings files is manually deleted
pub fn remove_broken_symlinks(args: &mut crate::cli::Cli) {
log::info!("Starting broken symlink removal task");
if let Err(e) = enforce_working_symlinks(&args.folder) {
log::error!("Error in broken symlink task: {}", e);
}
log::info!("Completed broken symlink removal task");
}
fn enforce_working_symlinks(root: impl AsRef<std::path::Path>) -> std::io::Result<()> {
for dir_entry in walkdir::WalkDir::new(root.as_ref().join(crate::consts::SETTING_FOLDER)) {
let dir_entry = dir_entry?;
match check_symlink(dir_entry.path()) {
Ok(true) => {},
Ok(false) => {
log::info!("Symlink {} seems broken, removing...", dir_entry.path().display());
if let Err(e) = std::fs::remove_file(dir_entry.path()) {
log::warn!("Failed to delete broken symlink {}: {}", dir_entry.path().display(), e);
}
},
Err(symlink_e) => {
log::info!("Thing {} seems broken, removing... err: {}", dir_entry.path().display(), symlink_e);
if let Err(e) = std::fs::remove_file(dir_entry.path()) {
log::warn!("Failed to delete broken thing {}: {}", dir_entry.path().display(), e);
}
}
}
}
Ok(())
}
fn check_symlink(path: impl AsRef<std::path::Path>) -> std::io::Result<bool> {
if path.as_ref().is_symlink() {
Ok(path.as_ref().read_link().is_ok_and(|link| link.exists()))
} else {
Ok(true)
}
}

View file

@ -1,47 +0,0 @@
use std::thread::{spawn, JoinHandle, sleep};
use std::time::{Duration, Instant};
pub struct TaskRunner<C: Send + 'static, F: FnMut(&mut C) + Send + 'static> {
task: F,
context: C,
period: Option<Duration>,
}
impl <C: Send + 'static, F: FnMut(&mut C) + Send + 'static> TaskRunner<C, F> {
pub fn new(c: C, f: F, period: Duration) -> Self {
Self {
task: f,
context: c,
period: Some(period),
}
}
/*
pub fn new_oneshot(c: C, f: F) -> Self {
Self {
task: f,
context: c,
period: None,
}
}*/
pub fn run(mut self) -> JoinHandle<()> {
if let Some(period) = self.period {
spawn(move || {
let mut pre_task;
let mut after_task;
loop {
pre_task = Instant::now();
(self.task)(&mut self.context);
after_task = Instant::now();
sleep(period - (after_task.duration_since(pre_task)));
}
})
} else {
spawn(move || {
(self.task)(&mut self.context);
})
}
}
}

View file

@ -1,114 +0,0 @@
use crate::file_util;
pub fn remove_similar_user_uploads(args: &mut crate::cli::Cli) {
log::info!("Starting user antispam task");
if let Err(e) = enforce_user_dirs(&args.folder, args.duplicates) {
log::error!("Error in user antispam task: {}", e);
}
log::info!("Completed user antispam task");
}
fn enforce_user_dirs(root: impl AsRef<std::path::Path>, keep_duplicates: usize) -> std::io::Result<()> {
let mut to_remove: Vec<community_settings_core::v1::Metadata> = Vec::new();
let mut seen_names = std::collections::HashMap::<String, Vec<community_settings_core::v1::Metadata>>::new();
for dir_entry in root.as_ref()
.join(crate::consts::SETTING_FOLDER)
.join(crate::consts::USER_ID_FOLDER)
.read_dir()? {
match dir_entry {
Ok(dir_entry) => {
log::info!("Scanning {} for user antispam", dir_entry.path().display());
if dir_entry.file_type()?.is_dir() {
seen_names.clear();
to_remove.clear();
for user_entry in dir_entry.path().read_dir()? {
match user_entry {
Ok(user_entry) => {
let f_path = user_entry.path();
if let Some(ext) = f_path.extension() {
if ext == crate::consts::RON_EXTENSION {
let reader = std::io::BufReader::new(std::fs::File::open(&f_path)?);
match ron::de::from_reader::<_, community_settings_core::v1::Metadata>(reader) {
Ok(x) => {
let sani_name = sanitise_name(&x.name);
if let Some(seen_in_ids) = seen_names.get_mut(&sani_name) {
seen_in_ids.push(x);
} else {
seen_names.insert(sani_name, vec![x]);
}
},
Err(e) => log::debug!("Error while reading {}: {}", f_path.display(), e)
}
}
}
},
Err(e) => log::warn!("Skipping file in {} for user antispam task due to error: {}", dir_entry.path().display(), e),
}
}
for seen in seen_names.values_mut() {
seen.sort_by_key(|meta| meta.get_id()); // sort lowest to highest
// keep the highest id (i.e. latest uploaded) settings
for _ in 0..keep_duplicates {
seen.pop();
}
to_remove.append(seen);
}
log::info!("Found {} spammy entries in {}", to_remove.len(), dir_entry.path().display());
// remove settings (and related symlinks) from the filesystem
for meta in to_remove.iter() {
let filename_ron = file_util::filename(meta.get_id(), crate::consts::RON_EXTENSION);
let filename_json = file_util::filename(meta.get_id(), crate::consts::JSON_EXTENSION);
// delete tag symlinks
for tag in meta.tags.iter() {
let app_id_tag_folder = file_util::setting_folder_by_app_id_tag(root.as_ref(), meta.steam_app_id, tag);
let user_id_tag_folder = file_util::setting_folder_by_user_id_tag(root.as_ref(), meta.steam_user_id, tag);
let tag_folder = file_util::setting_folder_by_tag(root.as_ref(), tag);
let paths = [
app_id_tag_folder.join(&filename_json),
app_id_tag_folder.join(&filename_ron),
user_id_tag_folder.join(&filename_json),
user_id_tag_folder.join(&filename_ron),
tag_folder.join(&filename_json),
tag_folder.join(&filename_ron),
];
for path in paths {
if path.exists() {
if let Err(e) = std::fs::remove_file(&path) {
log::warn!("Failed to delete {}: {}", path.display(), e);
}
}
}
}
// delete first-order symlinks and finally the actual files
let app_id_folder = file_util::setting_folder_by_app_id(root.as_ref(), meta.steam_app_id);
let user_id_folder = file_util::setting_folder_by_user_id(root.as_ref(), meta.steam_user_id);
let paths = [
app_id_folder.join(&filename_json),
app_id_folder.join(&filename_ron),
user_id_folder.join(&filename_json),
user_id_folder.join(&filename_ron),
file_util::setting_path_by_id(root.as_ref(), meta.get_id(), crate::consts::JSON_EXTENSION),
file_util::setting_path_by_id(root.as_ref(), meta.get_id(), crate::consts::RON_EXTENSION),
];
for path in paths {
if path.exists() {
if let Err(e) = std::fs::remove_file(&path) {
log::warn!("Failed to delete {}: {}", path.display(), e);
}
}
}
}
} else {
log::info!("Encountered non-folder path in user dir: {}", dir_entry.path().display());
}
},
Err(e) => log::warn!("Skipping file for user antispam task due to error: {}", e),
}
}
Ok(())
}
fn sanitise_name(name: &str) -> String {
name.trim().to_lowercase()
}

View file

@ -1,217 +0,0 @@
use std::path::Path;
use crate::consts::*;
pub fn build_folder_layout(root: impl AsRef<Path>) -> std::io::Result<()> {
std::fs::create_dir_all(
root.as_ref()
.join(SETTING_FOLDER)
.join(ID_FOLDER)
)?;
std::fs::create_dir_all(
root.as_ref()
.join(SETTING_FOLDER)
.join(APP_ID_FOLDER)
)?;
std::fs::create_dir_all(
root.as_ref()
.join(SETTING_FOLDER)
.join(USER_ID_FOLDER)
)?;
std::fs::create_dir_all(
root.as_ref()
.join(SETTING_FOLDER)
.join(TAG_FOLDER)
)?;
Ok(())
}
pub fn rebuild_symlinks(root: impl AsRef<Path>) -> std::io::Result<()> {
for dir_entry in root.as_ref()
.join(SETTING_FOLDER)
.join(ID_FOLDER)
.read_dir()? {
let dir_entry = dir_entry?;
if dir_entry.file_type()?.is_file() {
let f_path = dir_entry.path();
if let Some(ext) = f_path.extension() {
if ext == RON_EXTENSION {
let reader = std::io::BufReader::new(std::fs::File::open(&f_path)?);
let setting: community_settings_core::v1::Metadata = match ron::de::from_reader(reader) {
Ok(x) => x,
Err(e) => {
log::debug!("Error while reading {}: {}", f_path.display(), e);
let e_msg = format!("{}", e);
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg));
}
};
let to_symlink = crate::file_util::symlinks(&root, &setting)?;
let path_ron_canon = f_path.canonicalize()?;
let path_json_canon = crate::file_util::setting_path_by_id(&root, setting.get_id(), JSON_EXTENSION).canonicalize()?;
for ron_link in to_symlink.ron {
if ron_link.exists() { continue; }
log::info!("Rebuilding symlink {} -> {}", ron_link.display(), path_ron_canon.display());
#[cfg(target_family = "windows")] // NOTE: windows support is untested and unmaintained
{
std::os::windows::fs::symlink_file(&path_ron_canon, &ron_link)?;
}
#[cfg(target_family = "unix")]
{
std::os::unix::fs::symlink(&path_ron_canon, &ron_link)?;
}
}
for json_link in to_symlink.json {
if json_link.exists() { continue; }
log::info!("Rebuilding symlink {} -> {}", json_link.display(), path_json_canon.display());
#[cfg(target_family = "windows")] // NOTE: windows support is untested and unmaintained
{
std::os::windows::fs::symlink_file(&path_json_canon, json_link)?;
}
#[cfg(target_family = "unix")]
{
std::os::unix::fs::symlink(&path_json_canon, json_link)?;
}
}
}
}
}
}
Ok(())
}
pub fn fix_symlinks(root: impl AsRef<Path>) -> std::io::Result<()> {
log::info!("root setttings folder: {} aka {} (absolute)", root.as_ref().display(), root.as_ref().canonicalize()?.display());
for dir_entry in root.as_ref()
.join(SETTING_FOLDER)
.join(APP_ID_FOLDER)
.read_dir()? {
let dir_entry = dir_entry?;
if dir_entry.file_type()?.is_dir() {
make_symlinks_absolute_in_dir(root.as_ref(), dir_entry.path())?;
}
}
for dir_entry in root.as_ref()
.join(SETTING_FOLDER)
.join(USER_ID_FOLDER)
.read_dir()? {
let dir_entry = dir_entry?;
if dir_entry.file_type()?.is_dir() {
make_symlinks_absolute_in_dir(root.as_ref(), dir_entry.path())?;
}
}
for dir_entry in root.as_ref()
.join(SETTING_FOLDER)
.join(TAG_FOLDER).read_dir()? {
let dir_entry = dir_entry?;
if dir_entry.file_type()?.is_dir() {
make_symlinks_absolute_in_dir(root.as_ref(), dir_entry.path())?;
}
}
Ok(())
}
fn make_symlinks_absolute_in_dir(root: impl AsRef<Path>, dir: impl AsRef<Path>) -> std::io::Result<()> {
let abs_root = root.as_ref().canonicalize()?;
assert!(abs_root.is_absolute());
for dir_entry in dir.as_ref()
.read_dir()? {
let dir_entry = dir_entry?;
if dir_entry.file_type()?.is_symlink() {
let path = dir_entry.path();
let link_path = path.read_link()?;
if !link_path.is_absolute() {
let new_link = abs_root.join(
link_path.strip_prefix(&root).expect("Symlinked path does not begin with root settings folder")
);
log::info!("Fixing {} -> {} to -> {}", path.display(), link_path.display(), new_link.display());
std::fs::remove_file(&path)?;
#[cfg(target_family = "windows")] // NOTE: windows support is untested and unmaintained
{
std::os::windows::fs::symlink_file(new_link, &path)?;
}
#[cfg(target_family = "unix")]
{
std::os::unix::fs::symlink(new_link, &path)?;
}
}else {
log::info!("Found already-absolute symlink {} -> {}", path.display(), link_path.display());
}
} else {
log::info!("Found non-symlink {}: {:?}", dir_entry.path().display(), dir_entry.file_type()?);
}
}
Ok(())
}
pub fn sync_ids(root: impl AsRef<Path>) -> std::io::Result<()> {
for dir_entry in root.as_ref()
.join(SETTING_FOLDER)
.join(ID_FOLDER)
.read_dir()? {
let dir_entry = dir_entry?;
let f_path = dir_entry.path();
if let Some(ext) = f_path.extension() {
let id = f_path.file_stem().map(|os| os.to_string_lossy().to_string()).unwrap();
if ext == RON_EXTENSION {
let reader = std::io::BufReader::new(std::fs::File::open(&f_path)?);
let mut setting: community_settings_core::v1::Metadata = match ron::de::from_reader(reader) {
Ok(x) => x,
Err(e) => {
log::debug!("Error while reading {}: {}", f_path.display(), e);
let e_msg = format!("{}", e);
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg));
}
};
if setting.id != id {
setting.id = id;
ron::ser::to_writer(std::fs::File::create(&f_path)?, &setting).unwrap();
}
} else if ext == JSON_EXTENSION {
let reader = std::io::BufReader::new(std::fs::File::open(&f_path)?);
let mut setting: community_settings_core::v1::Metadata = match serde_json::from_reader(reader) {
Ok(x) => x,
Err(e) => {
log::debug!("Error while reading {}: {}", f_path.display(), e);
let e_msg = format!("{}", e);
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e_msg));
}
};
if setting.id != id {
setting.id = id;
serde_json::to_writer(std::fs::File::create(&f_path)?, &setting).unwrap();
}
}
}
}
Ok(())
}
pub fn make_tag_subfolders(root: impl AsRef<Path>) -> std::io::Result<()> {
for dir_entry in root.as_ref()
.join(SETTING_FOLDER)
.join(USER_ID_FOLDER)
.read_dir()? {
let dir_entry = dir_entry?;
if dir_entry.metadata()?.is_dir() {
let tag_folder = dir_entry.path().join(TAG_FOLDER);
if !tag_folder.exists() {
std::fs::create_dir(&tag_folder)?;
}
}
}
for dir_entry in root.as_ref()
.join(SETTING_FOLDER)
.join(APP_ID_FOLDER)
.read_dir()? {
let dir_entry = dir_entry?;
if dir_entry.metadata()?.is_dir() {
let tag_folder = dir_entry.path().join(TAG_FOLDER);
if !tag_folder.exists() {
std::fs::create_dir(&tag_folder)?;
}
}
}
// TODO populate folders
Ok(())
}

View file

@ -5,4 +5,4 @@ echo "Container's IP address: `awk 'END{print $1}' /etc/hosts`"
cd /backend cd /backend
bash build-docker.sh sudo bash build-docker.sh

View file

@ -34,8 +34,8 @@ impl GenericBatteryLimit {
fn default_steam_deck() -> Self { fn default_steam_deck() -> Self {
Self { Self {
charge_rate: Some(RangeLimit { charge_rate: Some(RangeLimit {
min: Some(0), min: Some(250),
max: Some(100), max: Some(2500),
}), }),
charge_modes: vec![ charge_modes: vec![
"normal".to_owned(), "normal".to_owned(),

View file

@ -612,8 +612,8 @@
"provider": "GabeBoy", "provider": "GabeBoy",
"limits": { "limits": {
"charge_rate": { "charge_rate": {
"min": 0, "min": 250,
"max": 100 "max": 2500
}, },
"charge_modes": [ "charge_modes": [
"normal", "normal",
@ -878,8 +878,8 @@
"provider": "GabeBoySP", "provider": "GabeBoySP",
"limits": { "limits": {
"charge_rate": { "charge_rate": {
"min": 0, "min": 250,
"max": 100 "max": 2500
}, },
"charge_modes": [ "charge_modes": [
"normal", "normal",

View file

@ -32,30 +32,6 @@ pub enum ApiMessage {
UploadCurrentVariant(String, String), // SteamID, Steam username UploadCurrentVariant(String, String), // SteamID, Steam username
} }
impl core::fmt::Display for ApiMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Battery(x) => write!(f, "Battery;{}", x),
Self::Cpu(x) => write!(f, "Cpu;{}", x),
Self::Gpu(x) => write!(f, "Gpu;{}", x),
Self::General(x) => write!(f, "General;{}", x),
Self::OnResume => write!(f, "OnResume"),
Self::OnPluggedIn => write!(f, "OnPluggedIn"),
Self::OnUnplugged => write!(f, "OnUnplugged"),
Self::OnChargeChange(x) => write!(f, "OnChargeChange({:?})", x),
Self::PowerVibeCheck => write!(f, "PowerVibeCheck"),
Self::WaitForEmptyQueue(_) => write!(f, "WaitForEmptyQueue"),
Self::LoadSettings(path, name, variant, variant_name) => write!(f, "LoadSettings({}, {}, {}, {})", path, name, variant, variant_name),
Self::LoadVariant(variant, variant_name) => write!(f, "LoadVariant({}, {})", variant, variant_name),
Self::LoadMainSettings => write!(f, "LoadMainSettings"),
Self::LoadSystemSettings => write!(f, "LoadSystemSettings"),
Self::GetLimits(_) => write!(f, "GetLimits"),
Self::GetProvider(s, _) => write!(f, "GetProvider({})", s),
Self::UploadCurrentVariant(id, user) => write!(f, "UploadCurrentVariant(id: {}, user: {})", id, user),
}
}
}
pub enum BatteryMessage { pub enum BatteryMessage {
SetChargeRate(Option<u64>), SetChargeRate(Option<u64>),
GetChargeRate(Callback<Option<u64>>), GetChargeRate(Callback<Option<u64>>),
@ -70,24 +46,6 @@ pub enum BatteryMessage {
GetChargeLimit(Callback<Option<f64>>), GetChargeLimit(Callback<Option<f64>>),
} }
impl core::fmt::Display for BatteryMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SetChargeRate(x) => write!(f, "SetChargeRate({:?})", x),
Self::GetChargeRate(_) => write!(f, "GetChargeRate"),
Self::SetChargeMode(x) => write!(f, "SetChargeMode({:?})", x),
Self::GetChargeMode(_) => write!(f, "GetChargeMode"),
Self::ReadChargeFull(_) => write!(f, "ReadChargeFull"),
Self::ReadChargeNow(_) => write!(f, "ReadChargeNow"),
Self::ReadChargeDesign(_) => write!(f, "ReadChargeDesign"),
Self::ReadCurrentNow(_) => write!(f, "ReadCurrentNow"),
Self::ReadChargePower(_) => write!(f, "ReadChargePower"),
Self::SetChargeLimit(x) => write!(f, "SetChargeLimit({:?})", x),
Self::GetChargeLimit(_) => write!(f, "GetChargeLimit"),
}
}
}
impl BatteryMessage { impl BatteryMessage {
fn process(self, settings: &mut dyn TBattery) -> bool { fn process(self, settings: &mut dyn TBattery) -> bool {
let dirty = self.is_modify(); let dirty = self.is_modify();
@ -129,23 +87,6 @@ pub enum CpuMessage {
GetCpusGovernor(Callback<Vec<String>>), GetCpusGovernor(Callback<Vec<String>>),
} }
impl core::fmt::Display for CpuMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SetCpuOnline(i, x) => write!(f, "SetCpuOnline({}, {})", i, x),
Self::SetCpusOnline(x) => write!(f, "SetCpusOnline({:?})", x),
Self::SetSmt(x, _) => write!(f, "SetChargeMode({})", x),
Self::GetSmt(_) => write!(f, "GetSmt"),
Self::GetCpusOnline(_) => write!(f, "GetCpusOnline"),
Self::SetClockLimits(x, y) => write!(f, "SetClockLimits({}, {:?})", x, y),
Self::GetClockLimits(x, _) => write!(f, "GetClockLimits({})", x),
Self::SetCpuGovernor(i, x) => write!(f, "SetCpuGovernor({}, {})", i, x),
Self::SetCpusGovernor(x) => write!(f, "SetCpusGovernor({:?})", x),
Self::GetCpusGovernor(_) => write!(f, "GetCpusGovernor"),
}
}
}
impl CpuMessage { impl CpuMessage {
fn process(self, settings: &mut dyn TCpus) -> bool { fn process(self, settings: &mut dyn TCpus) -> bool {
let dirty = self.is_modify(); let dirty = self.is_modify();
@ -265,19 +206,6 @@ pub enum GpuMessage {
GetMemoryClock(Callback<Option<u64>>), GetMemoryClock(Callback<Option<u64>>),
} }
impl core::fmt::Display for GpuMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SetPpt(x, y) => write!(f, "SetPpt(fast {:?}, slow {:?})", x, y),
Self::GetPpt(_) => write!(f, "GetPpt"),
Self::SetClockLimits(x) => write!(f, "SetClockLimits({:?})", x),
Self::GetClockLimits(_) => write!(f, "GetClockLimits"),
Self::SetMemoryClock(x) => write!(f, "SetMemoryClock({:?})", x),
Self::GetMemoryClock(_) => write!(f, "GetMemoryClock"),
}
}
}
impl GpuMessage { impl GpuMessage {
fn process(self, settings: &mut dyn TGpu) -> bool { fn process(self, settings: &mut dyn TGpu) -> bool {
let dirty = self.is_modify(); let dirty = self.is_modify();
@ -314,21 +242,6 @@ pub enum GeneralMessage {
ApplyNow, ApplyNow,
} }
impl core::fmt::Display for GeneralMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SetPersistent(x) => write!(f, "SetPersistent({})", x),
Self::GetPersistent(_) => write!(f, "GetPersistent"),
Self::GetCurrentProfileName(_) => write!(f, "GetCurrentProfileName"),
Self::GetPath(_) => write!(f, "GetPath"),
Self::GetCurrentVariant(_) => write!(f, "GetCurrentVariant"),
Self::GetAllVariants(_) => write!(f, "GetAllVariants"),
Self::AddVariant(variant, _) => write!(f, "AddVariant(name: `{}` [...])", variant.name),
Self::ApplyNow => write!(f, "ApplyNow"),
}
}
}
impl GeneralMessage { impl GeneralMessage {
fn process(self, settings: &mut dyn TGeneral) -> bool { fn process(self, settings: &mut dyn TGeneral) -> bool {
let dirty = self.is_modify(); let dirty = self.is_modify();
@ -372,31 +285,20 @@ fn print_errors(call_name: &str, errors: Vec<crate::settings::SettingError>) {
log::error!("Settings {}() err:\n{}", call_name, err_list); log::error!("Settings {}() err:\n{}", call_name, err_list);
} }
fn print_messages(msgs: &Vec<String>) {
let mut log_msg = String::new();
for msg in msgs.iter() {
//use core::fmt::Write;
write!(log_msg, "{}, ", msg).unwrap();
}
log::info!("Processed messages: [{}]", log_msg);
}
impl ApiMessageHandler { impl ApiMessageHandler {
pub fn process_forever(&mut self, settings: &mut Settings) { pub fn process_forever(&mut self, settings: &mut Settings) {
crate::utility::ioperm_power_ec(); crate::utility::ioperm_power_ec();
let mut dirty_echo = true; // set everything twice, to make sure PowerTools wins on race conditions //let mut dirty_echo = true; // set everything twice, to make sure PowerTools wins on race conditions
while let Ok(msg) = self.intake.recv() { while let Ok(msg) = self.intake.recv() {
let mut messages = vec![msg.to_string()]; // keep messages for logging
let mut dirty = self.process(settings, msg); let mut dirty = self.process(settings, msg);
while let Ok(msg) = self.intake.try_recv() { while let Ok(msg) = self.intake.try_recv() {
messages.push(msg.to_string());
dirty |= self.process(settings, msg); dirty |= self.process(settings, msg);
} }
if dirty if dirty
|| dirty_echo /*|| dirty_echo */
{ {
dirty_echo = dirty; // echo only once //dirty_echo = dirty; // echo only once
print_messages(&messages);
// run on_set // run on_set
if let Err(e) = settings.on_set() { if let Err(e) = settings.on_set() {
print_errors("on_set", e); print_errors("on_set", e);

View file

@ -4,7 +4,7 @@ use std::sync::{Arc, Mutex};
use sysfuss::capability::attributes; use sysfuss::capability::attributes;
use sysfuss::{ use sysfuss::{
HwMonAttribute, HwMonAttributeItem, HwMonAttributeType, HwMonPath, PowerSupplyAttribute, HwMonAttribute, HwMonAttributeItem, HwMonAttributeType, HwMonPath, PowerSupplyAttribute,
PowerSupplyPath, SysEntity, SysEntityAttributesExt, SysAttributeExt, PowerSupplyPath, SysAttribute, SysEntity, SysEntityAttributesExt,
}; };
use limits_core::json_v2::GenericBatteryLimit; use limits_core::json_v2::GenericBatteryLimit;
@ -230,12 +230,12 @@ const HWMON_NEEDS: &[HwMonAttribute] = &[
]; ];
const MAX_BATTERY_CHARGE_RATE_ATTR: HwMonAttribute = const MAX_BATTERY_CHARGE_RATE_ATTR: HwMonAttribute =
HwMonAttribute::custom("max_battery_charge_rate"); HwMonAttribute::custom("maximum_battery_charge_rate");
const MAX_BATTERY_CHARGE_LEVEL_ATTR: HwMonAttribute = const MAX_BATTERY_CHARGE_LEVEL_ATTR: HwMonAttribute =
HwMonAttribute::custom("max_battery_charge_level"); HwMonAttribute::custom("max_battery_charge_level");
const MAX_CHARGE_RATE: u64 = 100; const MAX_CHARGE_RATE: u64 = 2500;
const MIN_CHARGE_RATE: u64 = 0; const MIN_CHARGE_RATE: u64 = 250;
impl Battery { impl Battery {
fn find_battery_sysfs(root: Option<impl AsRef<std::path::Path>>) -> PowerSupplyPath { fn find_battery_sysfs(root: Option<impl AsRef<std::path::Path>>) -> PowerSupplyPath {
@ -325,7 +325,7 @@ impl Battery {
self.state.charge_rate_set = true; self.state.charge_rate_set = true;
let path = MAX_BATTERY_CHARGE_RATE_ATTR.path(&*self.sysfs_hwmon); let path = MAX_BATTERY_CHARGE_RATE_ATTR.path(&*self.sysfs_hwmon);
self.sysfs_hwmon self.sysfs_hwmon
.set(MAX_BATTERY_CHARGE_RATE_ATTR, format!("{}\n", charge_rate)) .set(MAX_BATTERY_CHARGE_RATE_ATTR, charge_rate)
.map_err(|e| SettingError { .map_err(|e| SettingError {
msg: format!("Failed to write to `{}`: {}", path.display(), e), msg: format!("Failed to write to `{}`: {}", path.display(), e),
setting: crate::settings::SettingVariant::Battery, setting: crate::settings::SettingVariant::Battery,
@ -336,10 +336,10 @@ impl Battery {
self.sysfs_hwmon self.sysfs_hwmon
.set( .set(
MAX_BATTERY_CHARGE_RATE_ATTR, MAX_BATTERY_CHARGE_RATE_ATTR,
format!("{}\n", self.limits self.limits
.charge_rate .charge_rate
.and_then(|lim| lim.max) .and_then(|lim| lim.max)
.unwrap_or(100)), .unwrap_or(2500),
) )
.map_err(|e| SettingError { .map_err(|e| SettingError {
msg: format!("Failed to write to `{}`: {}", path.display(), e), msg: format!("Failed to write to `{}`: {}", path.display(), e),

View file

@ -1,7 +1,8 @@
use std::convert::Into; use std::convert::Into;
use sysfuss::{ use sysfuss::{
capability::attributes, BasicEntityPath, HwMonPath, SysEntity, SysEntityAttributesExt, SysAttributeExt, capability::attributes, BasicEntityPath, HwMonPath, SysAttribute, SysEntity,
SysEntityAttributes, SysEntityAttributesExt,
}; };
use limits_core::json_v2::GenericGpuLimit; use limits_core::json_v2::GenericGpuLimit;
@ -150,7 +151,7 @@ impl Gpu {
if let super::Model::OLED = self.variant { if let super::Model::OLED = self.variant {
if let Ok(f) = self if let Ok(f) = self
.sysfs_card .sysfs_card
.read_value(&GPU_CLOCK_READOUT_ATTRIBUTE.to_owned()) .read_value(GPU_CLOCK_READOUT_ATTRIBUTE.to_owned())
{ {
let options = parse_pp_dpm_sclk(&String::from_utf8_lossy(&f)); let options = parse_pp_dpm_sclk(&String::from_utf8_lossy(&f));
return options return options
@ -191,7 +192,7 @@ impl Gpu {
fn quantize_memory_clock(&self, clock: u64) -> u64 { fn quantize_memory_clock(&self, clock: u64) -> u64 {
if let Ok(f) = self if let Ok(f) = self
.sysfs_card .sysfs_card
.read_value(&GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.to_owned()) .read_value(GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.to_owned())
{ {
let options = parse_pp_dpm_fclk(&String::from_utf8_lossy(&f)); let options = parse_pp_dpm_fclk(&String::from_utf8_lossy(&f));
// round (and find) nearest valid clock step // round (and find) nearest valid clock step
@ -237,7 +238,7 @@ impl Gpu {
{ {
let options_count = self let options_count = self
.sysfs_card .sysfs_card
.read_value(&GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.to_owned()) .read_value(GPU_MEMORY_DOWNCLOCK_ATTRIBUTE.to_owned())
.map(|b| parse_pp_dpm_fclk(&String::from_utf8_lossy(&b)).len()) .map(|b| parse_pp_dpm_fclk(&String::from_utf8_lossy(&b)).len())
.unwrap_or_else(|_| if is_oled { 4 } else { 2 }); .unwrap_or_else(|_| if is_oled { 4 } else { 2 });
let modifier = (options_count - 1) as u64; let modifier = (options_count - 1) as u64;

View file

@ -5,7 +5,7 @@
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
use sysfuss::{BasicEntityPath, SysEntityAttributesExt, SysAttributeExt}; use sysfuss::{BasicEntityPath, SysAttribute, SysEntityAttributesExt};
use crate::settings::SettingError; use crate::settings::SettingError;

View file

@ -1,13 +1,9 @@
#![allow(dead_code)] #![allow(dead_code)]
use std::sync::Mutex;
pub const JUPITER_HWMON_NAME: &'static str = "jupiter"; pub const JUPITER_HWMON_NAME: &'static str = "jupiter";
pub const STEAMDECK_HWMON_NAME: &'static str = "steamdeck_hwmon"; pub const STEAMDECK_HWMON_NAME: &'static str = "steamdeck_hwmon";
pub const GPU_HWMON_NAME: &'static str = "amdgpu"; pub const GPU_HWMON_NAME: &'static str = "amdgpu";
pub static THING_EC: Mutex<smokepatio::ec::unnamed_power::UnnamedPowerEC> = Mutex::new(smokepatio::ec::unnamed_power::UnnamedPowerEC::new());
pub fn range_min_or_fallback<I: Copy>( pub fn range_min_or_fallback<I: Copy>(
range: &Option<limits_core::json_v2::RangeLimit<I>>, range: &Option<limits_core::json_v2::RangeLimit<I>>,
fallback: I, fallback: I,
@ -29,25 +25,23 @@ pub fn card_also_has(card: &dyn sysfuss::SysEntity, extensions: &'static [&'stat
} }
const THINGS: &[u8] = &[ const THINGS: &[u8] = &[
0, 0, 0,
1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0,
0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1,
1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0,
0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0,
]; ];
const TIME_UNIT: std::time::Duration = std::time::Duration::from_millis(250); const TIME_UNIT: std::time::Duration = std::time::Duration::from_millis(200);
pub fn flash_led() { pub fn flash_led() {
use smokepatio::ec::ControllerSet; use smokepatio::ec::ControllerSet;
let mut ec = smokepatio::ec::unnamed_power::UnnamedPowerEC::new();
let mut ec = THING_EC.lock().unwrap();
for &code in THINGS { for &code in THINGS {
let on = code != 0; let on = code != 0;
let colour = if on { let colour = if on {
smokepatio::ec::unnamed_power::StaticColour::Red smokepatio::ec::unnamed_power::StaticColour::Red
} else { } else {
smokepatio::ec::unnamed_power::StaticColour::Disabled smokepatio::ec::unnamed_power::StaticColour::Off
}; };
if let Err(e) = ec.set(colour) { if let Err(e) = ec.set(colour) {
log::error!("Thing err: {}", e); log::error!("Thing err: {}", e);

View file

@ -69,8 +69,7 @@ pub fn chown_settings_dir() -> std::io::Result<()> {
.parse() .parse()
.unwrap_or(1000); .unwrap_or(1000);
log::info!( log::info!(
"chmod/chown {} for user `{}` ({})", "chmod/chown ~/.config/powertools for user `{}` ({})",
dir.display(),
deck_user, deck_user,
uid uid
); );

View file

@ -1,6 +1,6 @@
{ {
"name": "PowerTools", "name": "PowerTools",
"version": "2.0.3", "version": "2.0.0",
"description": "Power tweaks for power users", "description": "Power tweaks for power users",
"scripts": { "scripts": {
"build": "shx rm -rf dist && rollup -c", "build": "shx rm -rf dist && rollup -c",

View file

@ -85,7 +85,7 @@ export class Battery extends Component<backend.IdcProps> {
}} }}
/> />
{ get_value(CHARGE_RATE_BATT) != null && <SliderField { get_value(CHARGE_RATE_BATT) != null && <SliderField
label={tr("Maximum (%)")} label={tr("Maximum (mA)")}
value={get_value(CHARGE_RATE_BATT)} value={get_value(CHARGE_RATE_BATT)}
max={(get_value(LIMITS_INFO) as backend.SettingsLimits).battery.charge_current!.max} max={(get_value(LIMITS_INFO) as backend.SettingsLimits).battery.charge_current!.max}
min={(get_value(LIMITS_INFO) as backend.SettingsLimits).battery.charge_current!.min} min={(get_value(LIMITS_INFO) as backend.SettingsLimits).battery.charge_current!.min}

View file

@ -364,7 +364,6 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({}) => {
(ok: boolean) => { (ok: boolean) => {
backend.log(backend.LogLevel.Debug, "New settings variant ok? " + ok); backend.log(backend.LogLevel.Debug, "New settings variant ok? " + ok);
reload(); reload();
isVariantLoading = false;
backend.resolve(backend.waitForComplete(), (_) => { backend.resolve(backend.waitForComplete(), (_) => {
backend.log(backend.LogLevel.Debug, "Trying to tell UI to re-render due to new settings variant"); backend.log(backend.LogLevel.Debug, "Trying to tell UI to re-render due to new settings variant");
tryNotifyProfileChange(); tryNotifyProfileChange();

View file

@ -40,22 +40,18 @@ export class StoreResultsPage extends Component<{onNewVariant: () => void}> {
{ tr("No results") /* TODO translate */ } { tr("No results") /* TODO translate */ }
</Focusable>); </Focusable>);
} else { } else {
return ( // TODO
<div style={{ return (<Focusable
marginTop: "40px", style={{
marginBottom: "40px", display: "flex",
overflowY: "scroll", flexWrap: "wrap",
}}> justifyContent: "center",
<Focusable rowGap: "5px",
style={{ columnGap: "5px",
display: "flex", maxWidth: "100%",
flexWrap: "wrap", margin: "2em 0.5em",
justifyContent: "center", }}
rowGap: "0.5em", >
columnGap: "0.5em",
maxWidth: "100%",
}}
>
{ {
storeItems.map((meta: backend.StoreMetadata) => (<PanelSectionRow> storeItems.map((meta: backend.StoreMetadata) => (<PanelSectionRow>
<Focusable style={{ <Focusable style={{
@ -153,8 +149,7 @@ export class StoreResultsPage extends Component<{onNewVariant: () => void}> {
</Focusable> </Focusable>
</PanelSectionRow>)) </PanelSectionRow>))
} }
</Focusable> </Focusable>);
</div>);
} }
} else { } else {

Binary file not shown.

View file

@ -1,16 +0,0 @@
# TEMPLATE TITLE.
# Copyright (C) 2024 NGnius
# This file is distributed under the same license as the PowerTools package.
# NGnius (Graham) <ngniusness@gmail.com>, 2024.
msgid ""
msgstr ""
"Project-Id-Version: v1.1\n"
"Report-Msgid-Bugs-To: https://git.ngni.us/NG-SD-Plugins/PowerTools/PowerTools/issues\n"
"POT-Creation-Date: 2023-01-09 19:52-0500\n"
"PO-Revision-Date: 2024-05-07 18:42-0500\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: en-US\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"

View file

@ -5,7 +5,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: v1.1\n" "Project-Id-Version: v1.1\n"
"Report-Msgid-Bugs-To: https://git.ngni.us/NG-SD-Plugins/PowerTools/issues\n" "Report-Msgid-Bugs-To: https://github.com/NGnius/PowerTools/issues\n"
"POT-Creation-Date: 2023-01-09 19:52-0500\n" "POT-Creation-Date: 2023-01-09 19:52-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"