From 363c6a18c43c0bb800cde117a47538ef43b8f024 Mon Sep 17 00:00:00 2001 From: crapStone Date: Wed, 22 Jul 2020 18:47:26 +0200 Subject: [PATCH] initial commit --- .gitignore | 1 + 90-backlight.rules | 4 + Cargo.lock | 120 +++++++++++++++++++++++++ Cargo.toml | 16 ++++ README.md | 4 + build.rs | 14 +++ completions/lamp.bash | 73 +++++++++++++++ completions/lamp.fish | 10 +++ src/cli.rs | 82 +++++++++++++++++ src/controllers.rs | 204 ++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 61 +++++++++++++ 11 files changed, 589 insertions(+) create mode 100644 .gitignore create mode 100644 90-backlight.rules create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 build.rs create mode 100644 completions/lamp.bash create mode 100644 completions/lamp.fish create mode 100644 src/cli.rs create mode 100644 src/controllers.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/90-backlight.rules b/90-backlight.rules new file mode 100644 index 0000000..59ed1f1 --- /dev/null +++ b/90-backlight.rules @@ -0,0 +1,4 @@ +ACTION=="add", SUBSYSTEM=="backlight", RUN+="/bin/chgrp video /sys/class/backlight/%k/brightness" +ACTION=="add", SUBSYSTEM=="backlight", RUN+="/bin/chmod g+w /sys/class/backlight/%k/brightness" +ACTION=="add", SUBSYSTEM=="leds", RUN+="/bin/chgrp video /sys/class/leds/%k/brightness" +ACTION=="add", SUBSYSTEM=="leds", RUN+="/bin/chmod g+w /sys/class/leds/%k/brightness" diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..fa471f0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,120 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "clap" +version = "2.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "exitcode" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" + +[[package]] +name = "hermit-abi" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d737e0f947a1864e93d33fdef4af8445a00d1ed8dc0c8ddb73139ea6abf15" +dependencies = [ + "libc", +] + +[[package]] +name = "lamp" +version = "0.1.0" +dependencies = [ + "clap", + "exitcode", +] + +[[package]] +name = "libc" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" + +[[package]] +name = "vec_map" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" + +[[package]] +name = "winapi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5b39454 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "lamp" +version = "0.1.0" +authors = ["crapStone "] +edition = "2018" + +build = "build.rs" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[build-dependencies] +clap = "2.33" + +[dependencies] +clap = "2.33" +exitcode = "1.1.2" diff --git a/README.md b/README.md new file mode 100644 index 0000000..8f660aa --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# Lamp + +Lamp is a backlight control program written in Rust and inspired by +[acpibacklight](https://gitlab.com/wavexx/acpilight). diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..4365679 --- /dev/null +++ b/build.rs @@ -0,0 +1,14 @@ +use clap::Shell; + +include!("src/cli.rs"); + +fn main() { + let outdir = "completions"; // match env::var_os("OUT_DIR") { + // None => return, + // Some(outdir) => outdir, + // }; + let mut app = build_cli(); + app.gen_completions("lamp", Shell::Fish, outdir); + // app.gen_completions("lamp", Shell::Zsh, outdir); // TODO search for bug + app.gen_completions("lamp", Shell::Bash, outdir); +} diff --git a/completions/lamp.bash b/completions/lamp.bash new file mode 100644 index 0000000..06a16e6 --- /dev/null +++ b/completions/lamp.bash @@ -0,0 +1,73 @@ +_lamp() { + local i cur prev opts cmds + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + cmd="" + opts="" + + for i in ${COMP_WORDS[@]} + do + case "${i}" in + lamp) + cmd="lamp" + ;; + + *) + ;; + esac + done + + case "${cmd}" in + lamp) + opts=" -g -z -f -l -h -V -s -i -d -t --get --zero --full --list --help --version --set --increase --decrease --type " + if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + + --set) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -s) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --increase) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -i) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --decrease) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -d) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --type) + COMPREPLY=($(compgen -W "raw lin log" -- "${cur}")) + return 0 + ;; + -t) + COMPREPLY=($(compgen -W "raw lin log" -- "${cur}")) + return 0 + ;; + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; + + esac +} + +complete -F _lamp -o bashdefault -o default lamp diff --git a/completions/lamp.fish b/completions/lamp.fish new file mode 100644 index 0000000..1b0d532 --- /dev/null +++ b/completions/lamp.fish @@ -0,0 +1,10 @@ +complete -c lamp -n "__fish_use_subcommand" -s s -l set -d 'Sets brightness to given value' +complete -c lamp -n "__fish_use_subcommand" -s i -l increase -d 'Increases brightness' +complete -c lamp -n "__fish_use_subcommand" -s d -l decrease -d 'Decreases brightness' +complete -c lamp -n "__fish_use_subcommand" -s t -l type -d 'choose controller type' -r -f -a "raw lin log" +complete -c lamp -n "__fish_use_subcommand" -s g -l get -d 'Prints current brightness value' +complete -c lamp -n "__fish_use_subcommand" -s z -l zero -d 'Sets brightness to lowest value' +complete -c lamp -n "__fish_use_subcommand" -s f -l full -d 'Sets brightness to highest value' +complete -c lamp -n "__fish_use_subcommand" -s l -l list -d 'Lists all available brightness and led controllers' +complete -c lamp -n "__fish_use_subcommand" -s h -l help -d 'Prints help information' +complete -c lamp -n "__fish_use_subcommand" -s V -l version -d 'Prints version information' diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..fd665e2 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,82 @@ +use clap::{App, Arg, ArgGroup, ArgMatches}; + +pub fn build_cli() -> App<'static, 'static> { + App::new("RS Light") + .version("1.0") + .author("crapStone ") + .about("Utility to interact with backlight") + .arg( + Arg::with_name("set") + .short("s") + .long("set") + .value_name("VALUE") + .help("Sets brightness to given value") + .takes_value(true), + ) + .arg( + Arg::with_name("inc") + .short("i") + .long("increase") + .value_name("PERCENT") + .help("Increases brightness") + .takes_value(true), + ) + .arg( + Arg::with_name("dec") + .short("d") + .long("decrease") + .value_name("PERCENT") + .help("Decreases brightness") + .takes_value(true), + ) + .arg( + Arg::with_name("get") + .short("g") + .long("get") + .help("Prints current brightness value"), + ) + .arg( + Arg::with_name("zer") + .short("z") + .long("zero") + .help("Sets brightness to lowest value"), + ) + .arg( + Arg::with_name("ful") + .short("f") + .long("full") + .help("Sets brightness to highest value"), + ) + .group(ArgGroup::with_name("brightness_control").args(&["set", "inc", "dec", "get", "zer", "ful"])) + .arg( + Arg::with_name("list") + .short("l") + .long("list") + .help("Lists all available brightness and led controllers") + .conflicts_with_all(&["brightness_control"]), + ) + .arg( + Arg::with_name("ctrl_type") + .short("t") + .long("type") + .value_name("controller_type") + .takes_value(true) + .possible_values(&["raw", "lin", "log"]) + .default_value("lin") + .help("choose controller type") + .long_help( + r#"You can choose between these controller types: +raw: uses the raw values found in the device files +lin: uses percentage values (0.0 - 1.0) with a linear curve for the actual brightness +log: uses percentage values (0.0 - 1.0) with a logarithmic curve for the actual brightness + the perceived brightness for the human eyes should be linear with this controller +"#, + ), + ) +} + +/// Creates a argument parser with [clap](../clap/index.html) and returns a `Box` with the +/// [matches](../clap/struct.ArgMatches.html). +pub fn parse_args<'a>() -> Box> { + Box::new(build_cli().get_matches()) +} diff --git a/src/controllers.rs b/src/controllers.rs new file mode 100644 index 0000000..79e8342 --- /dev/null +++ b/src/controllers.rs @@ -0,0 +1,204 @@ +use std::collections::HashMap; +use std::fs::{File, OpenOptions}; +use std::io::prelude::*; +use std::path::{Path, PathBuf}; +use std::process::exit; + +use exitcode; + +const SYS_PATHS: [&str; 2] = ["/sys/class/backlight", "/sys/class/leds"]; + +pub trait Controller { + fn get_brightness(&self) -> i32; + fn get_max_brightness(&self) -> i32; + fn set_brightness(&self, value: i32); + + fn check_brightness_value(&self, value: i32) { + if value > self.get_max_brightness() { + eprintln!( + "brightness value too high: {} > {}", + value, + self.get_max_brightness() + ); + exit(exitcode::DATAERR); + } else if value < 0 { + eprintln!("brightness value too low: {}", value); + exit(exitcode::DATAERR); + } + } +} + +pub struct RawController { + path: Box, +} + +impl RawController { + pub fn new(path: Box) -> Self { + Self { path: path } + } +} + +impl Controller for RawController { + fn get_brightness(&self) -> i32 { + read_file_to_int(self.path.join("brightness")) + } + + fn get_max_brightness(&self) -> i32 { + read_file_to_int(self.path.join("max_brightness")) + } + + fn set_brightness(&self, value: i32) { + self.check_brightness_value(value); + + let path = self.path.join("brightness"); + + let mut file = match OpenOptions::new().write(true).read(true).open(&path) { + Err(why) => { + eprintln!("couldn't open '{}': {:?}", &path.display(), why.kind()); + exit(exitcode::OSFILE); + } + Ok(file) => file, + }; + + match write!(file, "{}", value) { + Ok(_) => {} + Err(err) => { + eprintln!( + "could not write '{}' to file '{}': {:?}", + value, + &path.display(), + err.kind() + ); + exit(exitcode::OSFILE); + } + }; + } +} + +pub struct LinController { + parent_controller: RawController, +} + +impl LinController { + pub fn new(path: Box) -> Self { + Self { + parent_controller: RawController::new(path), + } + } +} + +impl Controller for LinController { + fn get_brightness(&self) -> i32 { + ((self.parent_controller.get_brightness() as f64 + / self.parent_controller.get_max_brightness() as f64) + * self.get_max_brightness() as f64) as i32 + } + + fn get_max_brightness(&self) -> i32 { + 100 + } + + fn set_brightness(&self, value: i32) { + self.check_brightness_value(value); + + if value > self.get_max_brightness() { + eprintln!( + "brightness value too high! {} > {}", + value, + self.get_max_brightness() + ); + exit(exitcode::DATAERR); + } + + self.parent_controller.set_brightness( + (value * self.parent_controller.get_max_brightness()) / self.get_max_brightness(), + ) + } +} + +pub struct LogController { + parent_controller: RawController, +} + +impl LogController { + pub fn new(path: Box) -> Self { + Self { + parent_controller: RawController::new(path), + } + } +} + +impl Controller for LogController { + fn get_brightness(&self) -> i32 { + ((self.parent_controller.get_brightness() as f64).log10() + / (self.parent_controller.get_max_brightness() as f64).log10() + * self.get_max_brightness() as f64) as i32 + } + + fn get_max_brightness(&self) -> i32 { + 100 + } + + fn set_brightness(&self, value: i32) { + self.check_brightness_value(value); + + if value > self.get_max_brightness() { + eprintln!( + "brightness value too high! {} > {}", + value, + self.get_max_brightness() + ); + exit(exitcode::DATAERR); + } + + self.parent_controller.set_brightness(10f64.powf( + (value as f64 / self.get_max_brightness() as f64) + * (self.parent_controller.get_max_brightness() as f64).log10(), + ) as i32) + } +} + +fn read_file_to_int(path: PathBuf) -> i32 { + let mut file = match File::open(&path) { + Err(why) => { + eprintln!("couldn't open {}: {:?}", path.display(), why.kind()); + exit(exitcode::OSFILE); + } + Ok(file) => file, + }; + + let mut s = String::new(); + match file.read_to_string(&mut s) { + Err(why) => { + eprintln!("couldn't read {}: {:?}", path.display(), why.kind()); + exit(exitcode::OSFILE); + } + Ok(_) => return s.trim().parse().unwrap(), + } +} + +/// Searches through all paths in `SYS_PATHS` and creates a `HashMap` with the name and absolute path. +/// +/// It returns a `Tuple` of the default backlight name and the `HashMap`. +pub fn get_controllers() -> (String, HashMap>) { + let mut controllers: HashMap> = HashMap::new(); + + let mut default = None; + + for path in SYS_PATHS.iter() { + if Path::new(path).exists() { + for name in Path::new(path).read_dir().unwrap() { + let name = name.unwrap().path(); + let key = String::from(name.file_name().unwrap().to_str().unwrap()); + + if default.is_none() { + default = Some(key.clone()); + } + + controllers.insert(key, Box::new(name)); + } + } + } + + (default.unwrap(), controllers) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..a90736b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,61 @@ +mod cli; +mod controllers; + +use std::process::exit; + +use exitcode; + +use controllers::{Controller, LinController, LogController, RawController}; + +fn main() { + let matches = cli::parse_args(); + + let (default_ctrl, ctrls) = controllers::get_controllers(); + + let p = ctrls.get(&default_ctrl).unwrap().to_owned(); + let controller: Box = match matches.value_of("ctrl_type") { + Some("raw") => Box::new(RawController::new(p)), + Some("lin") => Box::new(LinController::new(p)), + Some("log") => Box::new(LogController::new(p)), + Some(_) | None => panic!(ERROR_MSG), + }; + + if matches.is_present("list") { + for ctrl in ctrls.keys() { + println!("{}", ctrl); + } + exit(exitcode::OK); + } else if let Some(value) = matches.value_of("set") { + let new_value = value.parse::().unwrap(); + controller.set_brightness(new_value); + } else if let Some(value) = matches.value_of("inc") { + let new_value = controller.get_brightness() + value.parse::().unwrap(); + controller.set_brightness(new_value.min(controller.get_max_brightness())); + } else if let Some(value) = matches.value_of("dec") { + let new_value = controller.get_brightness() - value.parse::().unwrap(); + controller.set_brightness(new_value.max(0)); + } else if matches.is_present("get") { + println!("{}", controller.get_brightness()); + } else if matches.is_present("zer") { + controller.set_brightness(0); + } else if matches.is_present("ful") { + controller.set_brightness(controller.get_max_brightness()); + } else { + panic!(ERROR_MSG); + } + + exit(exitcode::OK); +} + +// https://xkcd.com/2200/ +const ERROR_MSG: &str = r#" + ERROR! + + If you're seeing this, the code is in what I thought was an unreachable state. + + I could give you advice for what to do. but honestly, why should you trust me? + I clearly screwed this up. I'm writing a message that should never appear, + yet I know it will probably appear someday. + + On a deep level, I know I'm not up to this task. I'm so sorry. +"#;