blight-0.7.1/.cargo_vcs_info.json0000644000000001360000000000100123120ustar { "git": { "sha1": "32df8e68c7be848aaef23849ae6b10ca5471a996" }, "path_in_vcs": "" }blight-0.7.1/.gitignore000064400000000000000000000000101046102023000130610ustar 00000000000000/target blight-0.7.1/Cargo.lock0000644000000140650000000000100102730ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "bitflags" version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" [[package]] name = "blight" version = "0.7.1" dependencies = [ "colored", "fs4", ] [[package]] name = "colored" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" dependencies = [ "lazy_static", "windows-sys 0.48.0", ] [[package]] name = "errno" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "fs4" version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eeb4ed9e12f43b7fa0baae3f9cdda28352770132ef2e09a23760c29cae8bd47" dependencies = [ "rustix", "windows-sys 0.48.0", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "linux-raw-sys" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "rustix" version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.4", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" dependencies = [ "windows_aarch64_gnullvm 0.52.4", "windows_aarch64_msvc 0.52.4", "windows_i686_gnu 0.52.4", "windows_i686_msvc 0.52.4", "windows_x86_64_gnu 0.52.4", "windows_x86_64_gnullvm 0.52.4", "windows_x86_64_msvc 0.52.4", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" blight-0.7.1/Cargo.toml0000644000000022160000000000100103110ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "blight" version = "0.7.1" authors = ["Maaz Ahmed "] exclude = [ "*.png", ".github/workflows", ] description = "A hassle-free CLI backlight utility/library for Linux." readme = "README.md" keywords = [ "backlight", "CLI", "utility", "hybrid-gpu", "brightness", ] categories = [ "command-line-utilities", "os::linux-apis", ] license = "MIT" repository = "https://github.com/VoltaireNoir/blight" [profile.dist] inherits = "release" [profile.release] lto = true codegen-units = 1 panic = "abort" strip = true [dependencies.colored] version = "2.0.3" [dependencies.fs4] version = "0.6.6" features = ["sync"] blight-0.7.1/Cargo.toml.orig000064400000000000000000000022701046102023000137720ustar 00000000000000[package] name = "blight" description = "A hassle-free CLI backlight utility/library for Linux." categories = ["command-line-utilities", "os::linux-apis"] keywords = ["backlight", "CLI", "utility", "hybrid-gpu", "brightness"] authors = ["Maaz Ahmed "] repository = "https://github.com/VoltaireNoir/blight" license = "MIT" version = "0.7.1" edition = "2021" exclude = ["*.png", ".github/workflows"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] colored = "2.0.3" fs4 = { version = "0.6.6", features = ["sync"] } [profile.release] strip = true lto = true codegen-units = 1 panic = 'abort' [profile.dist] inherits = "release" # Config for 'cargo dist' [workspace.metadata.dist] # The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) cargo-dist-version = "0.0.7" # The preferred Rust toolchain to use in CI (rustup toolchain syntax) rust-toolchain-version = "1.68.0" # CI backends to support (see 'cargo dist generate-ci') ci = ["github"] # Target platforms to build apps for (Rust target-triple syntax) targets = ["x86_64-unknown-linux-gnu"] # The installers to generate for each app installers = [] blight-0.7.1/README.md000064400000000000000000000100671046102023000123650ustar 00000000000000# blight
![Generated using Dall-E 2](blightm.png) *(Credits: Sneha Sundar, for generating this lovely image for blight using DALLĀ·E 2)* [![Rust](https://github.com/VoltaireNoir/blight/actions/workflows/rust.yml/badge.svg)](https://github.com/VoltaireNoir/blight/actions/workflows/rust.yml) [![Crates.io](https://img.shields.io/crates/v/blight)](https://crates.io/crates/blight) [![Downloads](https://img.shields.io/crates/d/blight)](https://crates.io/crates/blight) ![License](https://img.shields.io/crates/l/blight)
> "And man said, 'let there b-light' and there was light." - Some Book 1:3 Primarily, a hassle-free CLI utility to manage backlight on Linux; one that plays well with hybrid GPU configuration and proprietary drivers. The parts which blight relies on to make backlight changes, are also exposed through the library aspect of this crate, which can be used like any other Rust library by using the command `cargo add blight` in your Rust project. The CLI utility, on the other hand, can be installed by running `cargo install blight`. > **Note** > This page contains documentation for the CLI. For library docs, visit [docs.rs](https://docs.rs/blight/). > **Warning** > For this program to run without root privileges, the user needs to be in the video group and might need udev rules to allow write access to brightness files. Read more about it [here](https://wiki.archlinux.org/title/Backlight#ACPI). You can gain required permissions by using the helper script that comes with blight by running `sudo blight setup` once or you could do it manually too. If not, you'd have to run the program with `sudo` every time. ## Screenshots ![](blight_s1.png) ![](blight_s2.png) ## About A lot of Linux backlight utilities often fail to detect the right backlight device to control in laptops that ship with Intel or Amd iGPUs and an Nvidia dGPU with proprietary drivers. This utility aims to solve that problem by prioritizing integrated graphic devices, followed by dedicated Nvdia GPU and ACPI kernel module. This means that you do not have to manually specify which device is currently active whenever you switch between your iGPU and dGPU using the MUX switch. Other than that, *blight* also implements the `sweep` functionality, which lets you change brightness in a smooth sweeping manner, rather than applying sudden jerky increments/decrements. In principle, blight should work on any GNU/Linux distro, and even on systems without hybrid GPU configuration. However, it has only been tested on Arch and Debian so far. Any feedback and bug reports will be greatly appreciated. ## Usage Set custom shortcuts using your distro settings or pair it with a hotkey daemon like [sxhkd](https://github.com/baskerville/sxhkd) and you'll be good to go. *blight* doesn't execute any code if another instance is already running, so do not worry about spamming the key that triggers it. ### Commands - Display help `blight` (quick help) or `blight help` - Display status `blight status` OR `blight status -d device_name` - Run first time setup script (for write permissions) `sudo blight setup` - List all backlight devices `blight list` - Increase brightness `blight inc 5` (increase by 5%) - Decrease brightness `blight dec 10` (decrease by 10%) - Increase/decrease brightness smoothly `blight inc 10 -s` OR `blight dec 10 --sweep` - Set custom brightness value `blight set 50` - Increase brightness for specific device `blight inc 2 -d nvidia_0` - Save brightness `blight save` OR `blight save -d amdgpu_bl0` - Restore brightness `blight restore` ## Install ### Using Cargo - `cargo install blight` - Binary will be compiled to `$HOME:.cargo/bin` ### Compile from Source - Clone repository - `cd cloned-repo` - `cargo build -r` ## Contribute Coding, for me, is a hobby and I'm very much new to Rust and to programming as a whole. So if you notice anything in the code that can be improved, do open an issue to voice your opinion and pass on your suggestions. If you want to improve the code directly, please raise a pull-request, I'd be happy to collaborate and work to improve this together. Cheers! blight-0.7.1/RELEASES.md000064400000000000000000000052111046102023000126260ustar 00000000000000# Version 0.7.1 ### Summary A minor bug fix release that changes the behavior of how errors are handled while acquiring a lock. Note: This release only contains changes to the CLI. ### Fixed - Handle errors while acquiring a lock instead of panicking (#9) # Version 0.7.0 ### Summary This release contains some breaking changes for the library users, like the type change from `u16 `to `u32` as part of a bug fix. Other than that, a `current_percent` method has been added to `Device` for convenience, and there's a minor CLI related change. The rest of the changes are all memory usage improvements and code refactoring for improved maintainability. ### Added - `Device::current_percent` method that returns brightness percentage (in contrast with `current` which returns the raw current value) ### Changed - All functions and methods that took and returned `u16` now use `u32` (breaking change) - `device_path` method now returns a `PathBuf `instead of `&Path` due to internal code changes (breaking change) - `blight status` now prints brightness percentage along with the raw value ### Improved - Significant reduction of heap allocations - Reduced code duplication and code refactored for maintainability ### Fixed - blight failing to work with devices that may use values larger than `u16::MAX` - #6 (Thanks pdamianik) # Version 0.6.0 ### Summary Fixed a major bug related to single instance check, which also changes the CLI behavior slightly. Improved error reporting in case of panics. Most changes in this release only affect the CLI side of things. ### Changed - CLI no longer returns an error if another instance is running. Instead, it waits for it to finish (#5) - blight no longer compiles on OSs other than Linux, as they are unsupported ### Improved - Custom panic handler now properly prints panic related info to the user to help with better bug reports ### Fixed - CLI falsely reporting that another instance is running (#4) # Version 0.5.0 ### Added - `Device::device_path` method, returns `&Path` to the location of the device in `sys/class/backlight/` - `Delay` type to customize the write frequency or delay between each write in `Device::sweep_write` - Custom panic hook (CLI) to print more helpful messages when a panic occurs ### Improved - `Device::reload` only reloads current value - `Device::sweep_write` updates brightness changes more efficiently (#2) ### Changed - Helper function `sweep` is now `Device::sweep_write` (#2) ### Fixed - Integer overflow while using sweep change (#1) - `Device::write_value` & `set_bl` silently ignoring or writing values larger than max supported (f30b3c5) - Stdout and Stderr message formatting inconsistencies blight-0.7.1/src/err.rs000064400000000000000000000050261046102023000130320ustar 00000000000000//! All blight library related errors in one place. See [BlibError] use std::{borrow::Cow, error::Error}; pub type BlResult = Result; /// All blight library related errors in one place. Every time one of the functions or methods of the library return an error, it'll always be one of this enum's variants. /// Some variants wrap additional error information and all of them have their separate Display trait implementations, containing a simple description of the error and possibly /// a tip to help the user fix it. #[derive(Debug)] pub enum BlibError { ReadBlDir(std::io::Error), NoDeviceFound, WriteNewVal { err: std::io::Error, dev: String }, ReadMax, ReadCurrent, SweepError(std::io::Error), ValueTooLarge { given: u32, supported: u32 }, } #[doc(hidden)] pub trait Tip: Error + 'static { fn tip(&self) -> Option>; } impl Tip for BlibError { fn tip(&self) -> Option> { use BlibError::WriteNewVal; match &self { WriteNewVal { dev, .. } => { let tip_msg = format!( "{main} '{dir}/{dev}/brightness'\n{extra}", main = "make sure you have write permission to the file", dir = super::BLDIR, extra = " Run `sudo blight setup` to install necessarry udev rules and add user to video group. or visit https://wiki.archlinux.org/title/Backlight#Hardware_interfaces if you'd like to do it manually.", ); Some(tip_msg.into()) } _ => None, } } } impl std::fmt::Display for BlibError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { use BlibError::*; match self { ReadBlDir(e) => write!(f, "failed to read {} directory\n{e}", super::BLDIR), NoDeviceFound => write!(f, "no known backlight device detected"), WriteNewVal { err, .. } => { write!(f, "failed to write to the brightness file ({err})",) } ReadCurrent => write!(f, "failed to read current brightness value"), ReadMax => write!(f, "failed to read max brightness value"), SweepError(err) => write!(f, "failed to sweep write to brightness file ({err})"), ValueTooLarge { given, supported } => write!( f, "provided value ({given}) is larger than the max supported value of {supported}" ), } } } impl std::error::Error for BlibError {} blight-0.7.1/src/lib.rs000064400000000000000000000505741046102023000130200ustar 00000000000000#![warn(clippy::pedantic)] //! # About //! blight is primarily a CLI backlight utility for Linux which is focused on providing hassle-free backlight control. //! However, the parts which blight relies on to make backlight changes, are also exposed through the library aspect of this crate, which can be used like any other Rust library //! by using the command `cargo add blight` in your Rust project. The CLI utility, on the other hand, can be installed by running `cargo install blight`. //! This documentation only covers the library aspect, for CLI related docs, visit the project's [Github repo](https://github.com/voltaireNoir/blight). //! //! Two features of blight that standout: //! 1. Prioritizing device detection in this order: iGPU>dGPU>ACPI>Fallback device. //! 2. Smooth backlight change by writing in increments/decrements of 1 with a few milliseconds of delay. \ //! > **IMPORTANT:** You need write permission for the file `/sys/class/backlight/{your_device}/brightness` to change brightness. //! > The CLI utility comes with a helper script that let's you gain access to the brightness file (which may not always work), which you can run by using the command `sudo blight setup`. //! > If you're only using blight as a dependency, you can read about gaining file permissions [here](https://wiki.archlinux.org/title/Backlight#ACPI). //! //! # Usage //! ```ignore //! use blight::{BlResult, Change, Device, Direction, Delay}; //! //! fn main() -> BlResult<()> { //! // Using the helper functions //! blight::change_bl(5, Change::Regular, Direction::Inc, None)?; // Increases brightness by 5% //! blight::set_bl(50, Some("nvidia_0".into()))?; // Sets brightness value (not percentage) to 50 //! //! // Doing it manually //! let mut dev = Device::new(None)?; //! let new = dev.calculate_change(5, Direction::Dec); // safely calculate value to write //! dev.write_value(new)?; // decreases brightness by 5% //! dev.reload(); // reloads current brightness value (important) //! let new = dev.calculate_change(5, Direction::Inc); //! dev.sweep_write(new, Delay::default()); // smoothly increases brightness by 5% //! Ok(()) //! } //! ``` #[cfg(not(target_os = "linux"))] compile_error!("blight is only supported on linux"); use err::BlibError; use std::{ borrow::Cow, error::Error, fs::{self, File}, io::prelude::*, ops::Deref, path::{Path, PathBuf}, thread, time::Duration, }; pub mod err; pub use err::BlResult; /// Linux backlight directory location. All backlight hardware devices appear here. pub const BLDIR: &str = "/sys/class/backlight"; const CURRENT_FILE: &str = "brightness"; const MAX_FILE: &str = "max_brightness"; /// This enum is used to specify the direction in which the backlight should be changed in the [``change_bl``] and [``Device::calculate_change``] functions. /// Inc -> Increase, Dec -> Decrease. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum Direction { Inc, Dec, } /// This enum is used to specify the kind of backlight change to carry out while calling the [``change_bl``] function. \ /// /// Regular change applies the calculated change directly, whereas the sweep change occurs in incremental steps. #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum Change { #[default] Regular, Sweep, } /// A wrapper type for [``std::time::Duration``] used for specifying delay between each iteration of the loop in [``Device::sweep_write``]. /// /// Delay implements the Default trait, which always returns a Delay of 25ms (recommended delay for smooth brightness transisions). /// The struct also provides the [``from_millis``][Delay::from_millis] constructor, if you'd like to set your own duration in milliseconds. /// If you'd like to set the delay duration using units other than milliseconds, then you can use the From trait to create Delay using [Duration][std::time::Duration]. #[derive(Debug, Clone, Copy)] pub struct Delay(Duration); impl From for Delay { fn from(value: Duration) -> Self { Self(value) } } impl Deref for Delay { type Target = Duration; fn deref(&self) -> &Self::Target { &self.0 } } impl Default for Delay { fn default() -> Self { Self(Duration::from_millis(25)) } } impl Delay { pub fn from_millis(millis: u64) -> Self { Self(Duration::from_millis(millis)) } } /// An abstraction of a backlight device containing a name, current and max backlight values, and some related functionality. /// /// A Device instance is created by using the [constructor][Device::new], values are read from /sys/class/backlight/ directory based on the detected GPU device. /// The constructor uses the default detection method unless a device name is passed as an argument. Based on whether a device is detected, the constructor will either return Some(Device) or None, /// if no device is detected. \ /// This is how the devices are priorirized: ``AmdGPU or Intel > Nvdia > ACPI > Any Fallback Device``, unless a device name is passed as an argument. /// # Examples /// ```ignore /// let bl = Device::new(None)?; /// bl.write_value(50)?; /// ``` #[derive(Debug, Clone)] pub struct Device { name: String, current: u32, max: u32, path: PathBuf, // Brightness file path } impl Device { /// Constructor for creating a [Device] instance. /// /// By default, it uses the priority detection method unless ``Some(device_name)`` is passed as an argument, then that name will be used to create an instance of that device if it exists. /// # Errors /// Possible errors that can result from this function include: /// * [``BlibError::NoDeviceFound``] /// * [``BlibError::ReadBlDir``] /// * [``BlibError::ReadCurrent``] /// * [``BlibError::ReadMax``] pub fn new(name: Option>) -> BlResult { let name = name .and_then(|n| Some(n)) .unwrap_or(Cow::from(Self::detect_device(BLDIR)?)); let mut path = Self::construct_path(BLDIR, &name); path.push(MAX_FILE); if !path.is_file() { return Err(BlibError::NoDeviceFound); }; let max = Self::read_value(&path).map_err(|_| BlibError::ReadMax)?; path.set_file_name(CURRENT_FILE); let current = Self::read_value(&path).map_err(|_| BlibError::ReadCurrent)?; Ok(Device { current, max, path, name: name.into_owned(), }) } fn construct_path(bldir: &str, device_name: &str) -> PathBuf { let mut buf = PathBuf::with_capacity(bldir.len() + device_name.len() + 1); buf.push(bldir); buf.push(device_name); buf } /// Returns the name of the current device pub fn name(&self) -> &str { &self.name } /// Returns the current brightness value of the current device pub fn current(&self) -> u32 { self.current } /// Returns the device's current brightness percentage (not rounded) pub fn current_percent(&self) -> f64 { (self.current as f64 / self.max as f64) * 100. } /// Returns the max brightness value of the current device pub fn max(&self) -> u32 { self.max } /// Returns absolute path that points to the device directory in `/sys/class/backlight` pub fn device_path(&self) -> PathBuf { let mut buf = self.path.to_path_buf(); buf.pop(); buf } fn detect_device(bldir: &str) -> BlResult { let dirs: Vec<_> = fs::read_dir(bldir) .map_err(BlibError::ReadBlDir)? .filter_map(|d| d.ok().map(|d| d.file_name())) .collect(); let (mut nv, mut ac): (Option, Option) = (None, None); for (i, entry) in dirs.iter().enumerate() { let name = entry.to_string_lossy(); if name.contains("amd") || name.contains("intel") { return Ok(name.into_owned()); } else if nv.is_none() && (name.contains("nvidia") | name.contains("nv")) { nv = Some(i); } else if ac.is_none() && name.contains("acpi") { ac = Some(i); } } let to_str = |i: usize| Ok(dirs[i].to_string_lossy().into_owned()); if let Some(nv) = nv { to_str(nv) } else if let Some(ac) = ac { to_str(ac) } else if !dirs.is_empty() { to_str(0) } else { Err(BlibError::NoDeviceFound) } } fn open_bl_file(&self) -> Result { fs::File::options().write(true).open(&self.path) } /// Reloads current value for the current device in place. /// # Panics /// The method panics if the current value fails to be read from the filesystem. pub fn reload(&mut self) { self.current = Device::read_value(&self.path).unwrap(); } fn read_value>(path: P) -> Result> { let mut buf = [0; 10]; // can hold string repr of u32::MAX fs::File::open(path)?.read(&mut buf)?; let pat: &[_] = &['\0', '\n', ' ']; let max: u32 = std::str::from_utf8(&buf)?.trim_matches(pat).parse()?; Ok(max) } /// Writes to the brightness file containted in /sys/class/backlight/ dir of the respective detected device, which will result in change of brightness if successful and if the chosen device is the correct one. /// # Errors /// - [``BlibError::WriteNewVal``] - on write failure pub fn write_value(&self, value: u32) -> BlResult<()> { if value > self.max { return Err(BlibError::ValueTooLarge { given: value, supported: self.max, }); } let convert = |err| BlibError::WriteNewVal { err, dev: self.name.clone(), }; write!(self.open_bl_file().map_err(convert)?, "{value}").map_err(convert)?; Ok(()) } /// Writes to the brightness file starting from the current value in a loop, increasing 1% on each iteration with some delay until target value is reached, /// creating a smooth brightness transition. /// /// This method takes a target value, which can be computed with the help of [``Device::calculate_change``] or can also be manually entered. /// The delay between each iteration of the loop can be set using the [``Delay``] type, or the default can be used by calling [``Delay::default()``], /// which sets the delay of 25ms/iter (recommended). /// /// Note: Nothing is written to the brightness file if the provided value is the same as current brightness value or is larger than the max brightness value. /// # Example /// ```ignore /// Device::new(None)? /// .sweep_write(50, Delay::default())?; /// ``` /// # Errors /// Possible errors that can result from this function include: /// * [``BlibError::SweepError``] pub fn sweep_write(&self, value: u32, delay: Delay) -> Result<(), BlibError> { let mut bfile = self.open_bl_file().map_err(BlibError::SweepError)?; let mut rate = (f64::from(self.max) * 0.01) as u32; let mut current = self.current; let dir = if value > self.current { Direction::Inc } else { Direction::Dec }; while !(current == value || value > self.max || (current == 0 && dir == Direction::Dec) || (current == self.max && dir == Direction::Inc)) { match dir { Direction::Inc => { if (current + rate) > value { rate = value - current; } current += rate; } Direction::Dec => { if rate > current { rate = current; } else if (current - rate) < value { rate = current - value; } current -= rate; } } bfile.rewind().map_err(BlibError::SweepError)?; write!(bfile, "{current}").map_err(BlibError::SweepError)?; thread::sleep(*delay); } Ok(()) } /// Calculates the new value to be written to the brightness file based on the provided step-size (percentage) and direction, /// using the current and max values of the detected GPU device. (Always guaranteed to be valid) /// /// For example, if the currecnt value is 10 and max is 100, and you want to increase it by 10% (step_size), /// the method will return 20, which can be directly written to the device. /// pub fn calculate_change(&self, step_size: u32, dir: Direction) -> u32 { let step: u32 = (self.max as f32 * (step_size as f32 / 100.0)) as u32; let change: u32 = match dir { Direction::Inc => self.current.saturating_add(step), Direction::Dec => self.current.saturating_sub(step), }; if change > self.max { self.max } else { change } } } /// A helper function to change backlight based on step-size (percentage), [Change] type and [Direction]. /// /// Regular change uses [calculated change][Device::calculate_change] value based on step size and is applied instantly. /// Sweep change on the other hand, occurs gradually, producing a fade or sweeping effect. (For more info, read about [``Device::sweep_write``]) /// > Note: No change is applied if the final calculated value is the same as current brightness value /// # Errors /// Possible errors that can result from this function include: /// * All errors that can result from [``Device::new``] /// * [``BlibError::WriteNewVal``] pub fn change_bl( step_size: u32, ch: Change, dir: Direction, device_name: Option>, ) -> Result<(), BlibError> { let device = Device::new(device_name)?; let change = device.calculate_change(step_size, dir); if change != device.current { match ch { Change::Sweep => device.sweep_write(change, Delay::default())?, Change::Regular => device.write_value(change)?, } } Ok(()) } /// A helper function which takes a brightness value and writes the value to the brightness file /// as long as the given value falls under the min and max bounds of the detected backlight device and is different from the current value. /// /// *Note: Unlike [change_bl], this function does not calculate any change, it writes the given value directly.* /// # Examples /// ```ignore /// blight::set_bl(15, None)?; /// ``` /// ```ignore /// blight::set_bl(50, Some("nvidia_0".into()))?; /// ```` /// # Errors /// Possible errors that can result from this function include: /// * All errors that can result from [``Device::new``] /// * [``BlibError::WriteNewVal``] /// * [``BlibError::ValueTooLarge``] pub fn set_bl(val: u32, device_name: Option>) -> Result<(), BlibError> { let device = Device::new(device_name)?; if val != device.current { device.write_value(val)?; } Ok(()) } #[cfg(test)] mod tests { use super::*; use std::error::Error; const TESTDIR: &str = "testbldir"; #[test] fn path_construction() { assert_eq!( Device::construct_path(BLDIR, "generic"), PathBuf::from("/sys/class/backlight/generic") ); } #[test] fn detecting_device_nvidia() { clean_up(); setup_test_env(&["nvidia_0", "generic"]).unwrap(); let name = Device::detect_device(TESTDIR); assert!(name.is_ok()); assert_eq!(name.unwrap(), "nvidia_0"); clean_up(); } #[test] fn detecting_device_amd() { clean_up(); setup_test_env(&["nvidia_0", "generic", "amdgpu_x"]).unwrap(); let name = Device::detect_device(TESTDIR); assert!(name.is_ok()); assert_eq!(name.unwrap(), "amdgpu_x"); clean_up(); } #[test] fn detecting_device_acpi() { clean_up(); setup_test_env(&["acpi_video0", "generic"]).unwrap(); let name = Device::detect_device(TESTDIR); assert!(name.is_ok()); assert_eq!(name.unwrap(), "acpi_video0"); clean_up(); } #[test] fn detecting_device_fallback() { clean_up(); setup_test_env(&["generic"]).unwrap(); let name = Device::detect_device(TESTDIR); assert!(name.is_ok()); assert_eq!(name.unwrap(), "generic"); clean_up(); } #[test] fn writing_value() { clean_up(); let name = "generic"; setup_test_env(&[name]).unwrap(); let d = Device { name: name.to_string(), max: 100, current: 50, path: test_path(name), }; d.write_value(100).unwrap(); let r = fs::read_to_string(format!("{TESTDIR}/generic/brightness")) .expect("failed to read test backlight value"); let res = r.trim(); assert_eq!("100", res, "Result was {res}"); clean_up(); } #[test] fn read_value() { clean_up(); let name = "generic"; setup_test_env(&[name]).unwrap(); assert_eq!(50, Device::read_value(test_path(name)).unwrap()); clean_up(); } #[test] fn current_value() { clean_up(); let name = "generic"; setup_test_env(&[name]).unwrap(); let current = Device::read_value(test_path(name)).unwrap(); assert_eq!(current, 50); clean_up(); } #[test] fn current_percent() { let device = Device { name: "".into(), current: 5, max: 255, path: test_path(""), }; assert_eq!(device.current_percent().round(), 2.0); } #[test] fn inc_calculation() { let d = Device { name: String::new(), current: 10, max: 100, path: test_path(""), }; let ch = d.calculate_change(10, Direction::Inc); assert_eq!(ch, 20); } #[test] fn dec_calculation() { let d = Device { name: String::new(), current: 30, max: 100, path: test_path(""), }; let ch = d.calculate_change(10, Direction::Dec); assert_eq!(ch, 20); } #[test] fn inc_calculation_max() { let d = Device { name: String::new(), current: 90, max: 100, path: test_path(""), }; let ch = d.calculate_change(20, Direction::Inc); assert_eq!(ch, 100); } #[test] fn dec_calculation_max() { let d = Device { name: String::new(), current: 10, max: 100, path: test_path(""), }; let ch = d.calculate_change(20, Direction::Dec); assert_eq!(ch, 0); } #[test] fn sweeping() { clean_up(); setup_test_env(&["generic"]).unwrap(); let mut d = test_device("generic"); d.sweep_write(100, Delay::default()).unwrap(); d.reload(); assert_eq!(d.current, 100); d.sweep_write(0, Delay::default()).unwrap(); d.reload(); assert_eq!(d.current, 0); clean_up(); } #[test] fn sweep_bounds() { clean_up(); setup_test_env(&["generic"]).unwrap(); let mut d = test_device("generic"); d.write_value(0).unwrap(); d.sweep_write(u32::MAX, Delay::default()).unwrap(); d.reload(); assert_eq!(d.current, 0); clean_up(); } fn setup_test_env(dirs: &[&str]) -> Result<(), Box> { fs::create_dir(TESTDIR)?; for dir in dirs { fs::create_dir(format!("{TESTDIR}/{dir}"))?; fs::write(format!("{TESTDIR}/{dir}/brightness"), "50")?; fs::write(format!("{TESTDIR}/{dir}/max_brightness"), "100")?; } Ok(()) } fn test_device(name: &str) -> Device { Device { name: name.into(), current: 50, max: 100, path: test_path(name), } } fn test_path(name: &str) -> PathBuf { let mut path = Device::construct_path(TESTDIR, name); path.push(CURRENT_FILE); path } fn clean_up() { if fs::read_dir(".") .unwrap() .any(|dir| dir.unwrap().file_name().as_os_str() == "testbldir") { fs::remove_dir_all(TESTDIR).expect("Failed to clean up testing backlight directory."); } } } blight-0.7.1/src/main.rs000064400000000000000000000005571046102023000131720ustar 00000000000000use std::env; mod utils; fn main() { utils::PanicReporter::init(); let config = match utils::parse(env::args().skip(1)) { Ok(c) => c, Err(e) => { utils::print_err(e); return; } }; match utils::execute(config) { Err(e) => utils::print_err(e), Ok(msg) => utils::print_ok(msg), } } blight-0.7.1/src/utils/setup.rs000064400000000000000000000074341046102023000145470ustar 00000000000000//! This module helps set up necessary udev rules for blight or the current user to gain write permission //! to the brightness file in /sys/class/backlight//brightness \n //! The write permission and ownership of the brightness file is assigned to the video group through the udev rules. //! The user is then added to the video group if they're not in the group already. use colored::*; use std::{ error::Error, fs, io::{self, ErrorKind}, path::PathBuf, process, }; const RULES: &str = r#"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""#; const UDEVFILE: &str = "/lib/udev/rules.d/90-blight.rules"; /// The function runs the setup. The udev file 90-blight.rules is placed in /lib/udev/.udev.rules.d/. /// The user is added to the 'video' group if they're not already in it. pub fn run() { println!("{}", "Running Setup".bold()); print!("UDEV Rules: "); match setup_rules() { RulesResult::Ok => println!("{}", "Ok".green()), RulesResult::Exists => println!("{}", "Ok (already in place)".green()), RulesResult::Err(err) => { if err.kind() == ErrorKind::PermissionDenied { println!("{}", "Failed. Run `blight setup` with sudo.".red()) } else { println!("{} {}", "Error:".red(), err); } } } print!("Video Group: "); match setup_group() { GroupResult::Exists => println!("{}", "Ok (already in group)".green()), GroupResult::Err(err) => println!("{} {}", "Error:".red(), err), GroupResult::UnknownErr => println!("{}", "Failed. Run `blight setup` with sudo.".red(),), GroupResult::Ok => println!("{}", "Ok".green()), } println!( "{}\n{}", "Recommended: Reboot your system once the setup completes successfully.".yellow(), "You can run `blight status` to check if you have gained write permissions.".yellow() ); } enum RulesResult { Ok, Exists, Err(std::io::Error), } fn setup_rules() -> RulesResult { let path = PathBuf::from(UDEVFILE); if path.exists() && fs::read_to_string(&path).unwrap().contains(RULES) { return RulesResult::Exists; } if let Err(err) = fs::write(UDEVFILE, RULES) { return RulesResult::Err(err); } RulesResult::Ok } enum GroupResult { Ok, Exists, Err(Box), UnknownErr, } fn setup_group() -> GroupResult { let user = String::from_utf8(process::Command::new("logname").output().unwrap().stdout).unwrap(); if in_group(&user) { return GroupResult::Exists; } if fs::read_to_string("/etc/group").unwrap().contains("video") { if let Err(err) = add_to_group(&user) { return GroupResult::Err(Box::new(err)); } } else { if let Err(err) = process::Command::new("groupadd") .arg("video") .stderr(process::Stdio::null()) .output() { return GroupResult::Err(Box::new(err)); } if let Err(err) = add_to_group(&user) { return GroupResult::Err(Box::new(err)); } } if in_group(&user) { GroupResult::Ok } else { GroupResult::UnknownErr } } fn in_group(user: &str) -> bool { String::from_utf8( process::Command::new("groups") .arg(user.trim()) .output() .expect("Failed to run groups command") .stdout, ) .unwrap() .contains("video") } fn add_to_group(user: &str) -> Result<(), io::Error> { process::Command::new("usermod") .args(["-aG", "video", user.trim()]) .stderr(process::Stdio::null()) .output()?; Ok(()) } blight-0.7.1/src/utils.rs000064400000000000000000000300211046102023000133730ustar 00000000000000use blight::{ err::{BlibError, Tip}, Change, Device, Direction::{self, Dec, Inc}, BLDIR, }; use colored::Colorize; use fs4::FileExt; use std::{ borrow::Cow, env, env::Args, error::Error, fs::{self, File, OpenOptions}, iter::Skip, path::PathBuf, }; mod setup; const SAVEDIR: &str = "/.local/share/blight"; const LOCKFILE: &str = "/tmp/blight.lock"; type DynError = Box; pub struct Config<'a> { command: Command, options: Options<'a>, } enum Command { Setup, Help, ShortHelp, Status, Save, Restore, List, Adjust { dir: Direction, value: u32 }, Set(u32), } #[derive(Default)] struct Options<'a> { device: Option>, sweep: Change, } impl Options<'_> { fn set(mut self, arg: String) -> Self { match arg.as_str() { "-d" | "--device" => self.device = Some("".into()), "-s" | "--sweep" => self.sweep = Change::Sweep, _ => { if let Some(d) = &mut self.device { if d.is_empty() { *d = Cow::from(arg); } } } } self } } pub fn parse<'a>(mut args: Skip) -> Result, DynError> { use BlightError::*; use Command::*; let option_parser = |args: Skip| -> Options { args.fold(Options::default(), |op, arg| op.set(arg)) }; let no_op = |cm: Command| (cm, Options::default()); let (command, options) = if let Some(arg) = args.next() { match arg.as_str() { "setup" => no_op(Setup), "help" => no_op(Help), "restore" => no_op(Restore), "list" => no_op(List), "status" => (Status, option_parser(args)), "save" => (Save, option_parser(args)), "set" => { let val: u32 = args .next() .ok_or(MissingValue)? .parse() .or(Err(InvalidValue))?; (Set(val), option_parser(args)) } ch @ ("inc" | "dec") => { let value: u32 = args .next() .ok_or(MissingValue)? .parse() .map_err(|_| InvalidValue)?; let dir = if ch == "inc" { Inc } else { Dec }; (Adjust { dir, value }, option_parser(args)) } _ => Err(UnrecognisedCommand)?, } } else { no_op(Command::ShortHelp) }; Ok(Config { command, options }) } type SuccessMessage = &'static str; pub fn execute(conf: Config) -> Result { use Command::*; match conf.command { Help => print_help(), ShortHelp => print_shelp(), List => print_devices(), Setup => setup::run(), Status => print_status(conf.options.device)?, Save => save(conf.options.device)?, Restore => restore()?, Set(v) => { let _lock = acquire_lock()?; blight::set_bl(v, conf.options.device)? } Adjust { dir, value } => { let _lock = acquire_lock()?; blight::change_bl(value, conf.options.sweep, dir, conf.options.device)? } }; Ok(gen_success_msg(&conf.command)) } #[derive(Debug)] pub enum BlightError { UnrecognisedCommand, MissingValue, InvalidValue, CreateSaveDir(PathBuf), WriteToSaveFile(PathBuf), ReadFromSave(std::io::Error), NoSaveFound, SaveParseErr, LockFailure(std::io::Error), } impl Tip for BlightError { fn tip(&self) -> Option> { use BlightError::*; match self { UnrecognisedCommand => Some("try 'blight help' to see all commands".into()), InvalidValue => Some("make sure the value is a valid positive integer".into()), NoSaveFound => Some("try using 'blight save' first".into()), MissingValue => { Some("try 'blight help' to see all commands and their supported args".into()) } ReadFromSave(_) => Some("make sure you have read permission for the save file".into()), SaveParseErr => Some("delete the save file and try save-restore again".into()), LockFailure(_) => { Some(format!("try manually removing the lock file: `{LOCKFILE}`").into()) } _ => None, } } } impl std::fmt::Display for BlightError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { use BlightError::*; match self { UnrecognisedCommand => write!(f, "unrecognised command entered"), MissingValue => write!(f, "required argument was not provided for the command"), InvalidValue => write!(f, "invalid value provided"), CreateSaveDir(loc) => write!(f, "failed to create save directory at {}", loc.display()), WriteToSaveFile(loc) => write!(f, "failed to write to save file at {}", loc.display()), ReadFromSave(err) => write!(f, "failed to read from save file\n{err}"), NoSaveFound => write!(f, "no save file found"), SaveParseErr => write!(f, "failed to parse saved brightness value"), LockFailure(err) => write!(f, "failed to acquire lock\n{err}"), } } } impl Error for BlightError {} pub fn print_err(e: DynError) { eprintln!("{} {e}", "Error".red().bold()); if let Some(tip) = e .downcast_ref::() .and_then(|e| e.tip()) .or(e.downcast_ref::().and_then(|e| e.tip())) { eprintln!("{} {tip}", "Tip".yellow().bold()) } } pub fn print_ok(msg: &str) { if !msg.is_empty() { println!("{} {msg}", "Success".green().bold()) } } fn gen_success_msg(cm: &Command) -> SuccessMessage { use Command::*; match cm { Save => "Current backlight state saved", Restore => "Saved backlight state restored", Set(_) => "Backlight value set", Adjust { .. } => "Backlight changed", _ => "", } } fn check_write_perm(device_name: &str, bldir: &str) -> Result<(), std::io::Error> { let path = format!("{bldir}/{device_name}/brightness"); fs::read_to_string(&path) .and_then(|contents| fs::write(&path, contents)) .and(Ok(())) } pub fn print_status(device_name: Option>) -> Result<(), BlibError> { let device = Device::new(device_name)?; let write_perm = match check_write_perm(device.name(), BLDIR) { Ok(_) => "Ok".green(), Err(err) => format!("{err}").red(), }; println!( "{}\nDetected device: {}\nWrite permission: {}\nCurrent brightness: {}, {}%\nMax brightness: {}", "Device status".bold(), device.name().green(), write_perm, device.current().to_string().green(), device.current_percent().round().to_string().green(), device.max().to_string().green() ); Ok(()) } pub fn print_devices() { println!("{}", "Detected Devices".bold()); fs::read_dir(BLDIR) .expect("Failed to read Backlight Directory") .for_each(|d| println!("{}", d.unwrap().file_name().to_string_lossy().green())); } pub fn print_help() { let title = "blight: A backlight utility for Linux that plays well with hybrid GPUs"; let quote = "\"And man said, \'let there b-light\' and there was light.\" - Some Book 1:3"; let flags = "Flags: sweep [--sweep, -s], dev [--device , -d ] Sweep flag lets you increase brightness gradually, resulting in a smooth change. Dev (short for device) flag lets you specify a backlight device target other than the default one."; let commands: String = [ ("inc [val] [flags: dev, sweep]", "-> increase brightness"), ("dec [val] [flags: dev, sweep]", "-> decrease brightness"), ("set [val] [flags: dev]", "-> set custom brightness value"), ( "save [flags: dev]", "-> save current brightness value to restore later", ), ("restore", "-> restore saved brightness value\n"), ( "setup", "-> installs udev rules and adds user to video group (run with sudo)", ), ("status [flags: dev]", "-> backlight device status"), ("list", "-> list all backlight devices"), ("help", "-> display help"), ] .into_iter() .map(|(c, e)| format!("{} {e}\n", c.green().bold())) .collect(); let exampels = "\ Examples: sudo blight setup blight status (show backlight device status info) blight inc 5 --sweep (increase brightness smoothly by 5%) blight set 10 (sets the brightness value to 10) blight inc 2 -s -d nvidia_0 (increases nvidia_0's brightness smoothly by 2%)"; println!( "{t}\n\n{quote}\n\n{f}\n\n{ct}\n{commands}\n{e}", t = title.blue().bold(), f = flags.magenta(), ct = "Commands".bold(), e = exampels.bright_yellow() ); } pub fn print_shelp() { let cc: String = [ ("inc [val]", "-> increase brightness by given value"), ("dec [val]", "-> decrease brightness by given value"), ("set [val]", "-> set custom brightness value"), ("status", "-> show backlight device info"), ("setup", "-> gain write permission to brightness file"), ] .into_iter() .map(|(c, e)| format!("{} {e}\n", c.green().bold())) .collect(); println!( "{t}\n\n{ct}\n{cc}\n{h}", t = "blight: A backlight utility for Linux".blue().bold(), ct = "Common Commands".bold(), h = "Use `blight help' to display all commands and options".yellow() ); } pub fn save(device_name: Option>) -> Result<(), DynError> { let device = Device::new(device_name)?; let mut savedir = PathBuf::from(env::var("HOME").unwrap() + SAVEDIR); if !savedir.exists() && fs::create_dir_all(&savedir).is_err() { return Err(BlightError::CreateSaveDir(savedir).into()); } savedir.push("blight.save"); fs::write(&savedir, format!("{} {}", device.name(), device.current())) .map_err(|_| BlightError::WriteToSaveFile(savedir))?; Ok(()) } pub fn restore() -> Result<(), DynError> { let save = PathBuf::from((env::var("HOME").unwrap() + SAVEDIR) + "/blight.save"); let restore = if save.is_file() { fs::read_to_string(save).map_err(BlightError::ReadFromSave)? } else { Err(BlightError::NoSaveFound)? }; let (device_name, val) = restore.split_once(' ').unwrap(); let device = Device::new(Some(device_name.into()))?; let value: u32 = val.parse().map_err(|_| BlightError::SaveParseErr)?; device.write_value(value)?; Ok(()) } pub struct PanicReporter; impl PanicReporter { pub fn init() { if !cfg!(debug_assertions) { std::panic::set_hook(Box::new(Self::report)); } } fn report(info: &std::panic::PanicInfo) { let tip = "This is unexpected behavior. Please report this issue at https://github.com/VoltaireNoir/blight/issues"; let payload = info.payload(); let cause = if let Some(pay) = payload.downcast_ref::<&str>() { pay.to_string() } else if let Some(pay) = payload.downcast_ref::() { pay.to_string() } else { "Unknown".to_owned() }; eprintln!("{} A panic occured", "Error".red().bold()); eprintln!("{} {cause}", "Reason".magenta().bold()); if let Some(loc) = info.location() { eprintln!("{} {}", "Location".blue().bold(), loc); } eprintln!("{} {tip}", "Tip".yellow().bold()); } } fn acquire_lock() -> Result { let file = OpenOptions::new() .write(true) .create(true) .open(LOCKFILE) .map_err(BlightError::LockFailure)?; if file.try_lock_exclusive().is_ok() { return Ok(file); } println!( "{} {}", "Status".magenta().bold(), "Waiting for another instance to finish" ); file.lock_exclusive().map_err(BlightError::LockFailure)?; Ok(file) }