diff --git a/Cargo.lock b/Cargo.lock index d51dd9e..ede91b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,13 +46,14 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.3" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" dependencies = [ "cfg-if", "once_cell", "version_check", + "zerocopy", ] [[package]] @@ -302,7 +303,7 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", ] [[package]] @@ -319,7 +320,7 @@ checksum = "7b2d0f03b3640e3a630367e40c468cb7f309529c708ed1d88597047b0e7c6ef7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", ] [[package]] @@ -505,7 +506,7 @@ checksum = "fdde5c9cd29ebd706ce1b35600920a33550e402fc998a2e53ad3b42c3c47a192" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", ] [[package]] @@ -641,7 +642,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", ] [[package]] @@ -1002,7 +1003,7 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3aef8ec3ae1b772f340170c65bf27d5b8c28f543a0116c844d2ac08d01123e7" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.7", "epaint", "log", "nohash-hasher", @@ -1095,7 +1096,7 @@ checksum = "eecf8589574ce9b895052fa12d69af7a233f99e6107f5cb8dd1044f2a17bfdcb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", ] [[package]] @@ -1116,7 +1117,7 @@ checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", ] [[package]] @@ -1126,7 +1127,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09333964d4d57f40a85338ba3ca5ed4716070ab184dcfed966b35491c5c64f3b" dependencies = [ "ab_glyph", - "ahash 0.8.3", + "ahash 0.8.7", "atomic_refcell", "bytemuck", "ecolor", @@ -1285,7 +1286,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", ] [[package]] @@ -1364,7 +1365,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", ] [[package]] @@ -1596,7 +1597,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", ] [[package]] @@ -1733,7 +1734,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", ] [[package]] @@ -2484,13 +2485,58 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +dependencies = [ + "num-traits", +] + [[package]] name = "num-integer" -version = "0.1.44" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" dependencies = [ "autocfg", + "num-integer", "num-traits", ] @@ -2501,6 +2547,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ "autocfg", + "num-bigint", "num-integer", "num-traits", ] @@ -2564,7 +2611,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", ] [[package]] @@ -3233,7 +3280,7 @@ checksum = "6f4c2c6ea4bc09b5c419012eafcdb0fcef1d9119d626c8f3a0708a5b92d38a70" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", ] [[package]] @@ -3255,7 +3302,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", ] [[package]] @@ -3398,7 +3445,7 @@ dependencies = [ [[package]] name = "solar-screen-brightness" -version = "2.1.0" +version = "2.2.0" dependencies = [ "anyhow", "brightness", @@ -3420,6 +3467,7 @@ dependencies = [ "itertools 0.11.0", "log", "nix 0.22.3", + "num", "png", "pollster", "serde", @@ -3502,9 +3550,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.28" +version = "2.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2" dependencies = [ "proc-macro2", "quote", @@ -3590,7 +3638,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", ] [[package]] @@ -3803,7 +3851,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", ] [[package]] @@ -4058,7 +4106,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", "wasm-bindgen-shared", ] @@ -4092,7 +4140,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.32", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4854,6 +4902,26 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + [[package]] name = "zune-inflate" version = "0.2.54" diff --git a/Cargo.toml b/Cargo.toml index b220e09..0ba73f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "solar-screen-brightness" -version = "2.1.0" +version = "2.2.0" authors = ["Jacob Halsey "] edition = "2021" build = "build.rs" @@ -32,6 +32,7 @@ human-repr = "1.1.0" image = "0.24.7" itertools = "0.11.0" log = "0.4.14" +num = "0.4.1" png = "0.17.10" pollster = "0.3.0" serde = { version = "1.0.110", features = ["derive"] } diff --git a/README.md b/README.md index a5af054..5fe3d4d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Build status](https://github.com/jacob-pro/solar-screen-brightness/actions/workflows/rust.yml/badge.svg)](https://github.com/jacob-pro/solar-screen-brightness/actions) -Varies screen brightness according to sunrise/sunset times. +Automatically and smoothly adjusts monitor brightness based on the sunrise/sunset times at your location. > #### New 2.0 Release! > @@ -19,10 +19,19 @@ automatically adjust screen brightness. This changes the screen brightness via monitor control APIs, whereas those utilities vary the colour temperature. +### How to Install + +For Windows, you can download pre-compiled binaries from +[Releases](https://github.com/jacob-pro/solar-screen-brightness/releases). + +If you are using Linux, please read the [Linux Guide](linux/README.md) + +There is also a CLI only version of the application available. + ### How to Use 1. An icon will appear in your tray when it is running. -2. Click on the icon to launch the console window. +2. Click on the icon to open the settings menu. 3. Use the menus to set: - Daytime and Nighttime brightness percentages. - Transition time (the time it takes to switch between the two brightness values at either sunset or sunrise). @@ -30,15 +39,6 @@ This changes the screen brightness via monitor control APIs, whereas those utili 4. Click save and this configuration will be applied and persisted to disk. 5. You can close the window, and it will continue to update your brightness in the background. -### How to Install - -For Windows, you can download pre-compiled binaries from -[Releases](https://github.com/jacob-pro/solar-screen-brightness/releases). - -If you are using Linux, please read the [Linux Guide](linux/README.md) - -There is also a CLI only version of the application available. - ## Screenshots ![](./screenshots/status.png) diff --git a/brightness.png b/brightness.png deleted file mode 100644 index b7a5a97..0000000 Binary files a/brightness.png and /dev/null differ diff --git a/screenshots/brightness.png b/screenshots/brightness.png index d8bdd46..af46cb9 100644 Binary files a/screenshots/brightness.png and b/screenshots/brightness.png differ diff --git a/screenshots/status.png b/screenshots/status.png index ef9a1ed..8b248ab 100644 Binary files a/screenshots/status.png and b/screenshots/status.png differ diff --git a/src/apply.rs b/src/apply.rs index 7347652..b71c0e6 100644 --- a/src/apply.rs +++ b/src/apply.rs @@ -101,8 +101,10 @@ pub fn apply_brightness( .iter() .map(MonitorOverrideCompiled::from) .collect::>(); - let now = SystemTime::now(); - let epoch_time_now = now.duration_since(UNIX_EPOCH).unwrap().as_secs() as i64; + let epoch_time_now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as i64; let sun = SunriseSunsetParameters::new(epoch_time_now, location.latitude, location.longitude) .calculate() .unwrap(); diff --git a/src/config.rs b/src/config.rs index 372289c..5d42472 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,7 +11,7 @@ use validator::Validate; const CONFIG_FILE_NAME: &str = "config.json"; -#[derive(Debug, Deserialize, Serialize, Validate, Clone)] +#[derive(Debug, Deserialize, Serialize, Validate, Clone, Copy, PartialEq)] pub struct Location { #[validate(range(min = -90, max = 90))] pub latitude: f64, diff --git a/src/event_watcher/windows.rs b/src/event_watcher/windows.rs index 459b692..fa958db 100644 --- a/src/event_watcher/windows.rs +++ b/src/event_watcher/windows.rs @@ -85,7 +85,7 @@ impl EventWatcher { DispatchMessageA(&message); } } - log::debug!("EventWatcher thread exiting"); + log::info!("EventWatcher thread exiting"); }); let hwnd = rx.recv().unwrap(); @@ -99,7 +99,7 @@ impl EventWatcher { impl Drop for EventWatcher { fn drop(&mut self) { log::info!("Stopping EventWatcher"); - unsafe { SendMessageW(self.hwnd, EXIT_LOOP, None, None) }; + unsafe { check_error(|| SendMessageW(self.hwnd, EXIT_LOOP, None, None)).unwrap() }; self.thread.take().unwrap().join().unwrap(); } } @@ -126,6 +126,7 @@ unsafe extern "system" fn wndproc( .unwrap(); } EXIT_LOOP => { + log::info!("Received EXIT_LOOP message"); PostQuitMessage(0); } WM_WTSSESSION_CHANGE => match wparam.0 as u32 { diff --git a/src/gui/brightness_settings.rs b/src/gui/brightness_settings.rs index ecc801f..cc8dc44 100644 --- a/src/gui/brightness_settings.rs +++ b/src/gui/brightness_settings.rs @@ -1,12 +1,53 @@ -use crate::config::SsbConfig; +use crate::calculator::calculate_brightness; +use crate::config::{Location, SsbConfig}; use crate::controller::Message; use crate::gui::app::{save_config, AppState, Page, SPACING}; +use chrono::{Duration, DurationRound, TimeZone}; +use egui::plot::{uniform_grid_spacer, GridInput, GridMark, Line, PlotBounds}; +use egui::widgets::plot::Plot; +use std::mem::take; +use std::time::{Instant, SystemTime, UNIX_EPOCH}; +use sunrise_sunset_calculator::SunriseSunsetParameters; use validator::Validate; pub struct BrightnessSettingsPage { brightness_day: u32, brightness_night: u32, transition_mins: u32, + plot: Option, +} + +struct PlotData { + points: Vec<[f64; 2]>, + generated_at: SystemTime, + brightness_day: u32, + brightness_night: u32, + transition_mins: u32, + location: Location, +} + +impl PlotData { + fn is_stale(&self, config: &SsbConfig) -> bool { + if let Some(location) = config.location { + if location != self.location { + return true; + } + } + if config.brightness_day != self.brightness_day { + return true; + } + if config.brightness_night != self.brightness_night { + return true; + } + if config.transition_mins != self.transition_mins { + return true; + } + let age = SystemTime::now().duration_since(self.generated_at).unwrap(); + if chrono::Duration::from_std(age).unwrap().num_minutes() > 5 { + return true; + } + false + } } impl BrightnessSettingsPage { @@ -15,6 +56,7 @@ impl BrightnessSettingsPage { brightness_day: config.brightness_day, brightness_night: config.brightness_night, transition_mins: config.transition_mins, + plot: None, } } @@ -65,5 +107,163 @@ impl Page for BrightnessSettingsPage { save_config(&mut config, &app_state.transitions); }; }); + + ui.add_space(SPACING); + self.render_plot(ui, app_state); + } +} + +const LINE_NAME: &str = "Brightness"; + +impl BrightnessSettingsPage { + fn render_plot(&mut self, ui: &mut egui::Ui, app_state: &mut AppState) { + let config = app_state.config.read().unwrap(); + + if let Some(location) = config.location { + self.plot = Some(match take(&mut self.plot) { + None => generate_plot_data( + location, + config.brightness_day, + config.brightness_night, + config.transition_mins, + ), + Some(x) if x.is_stale(&config) => generate_plot_data( + location, + config.brightness_day, + config.brightness_night, + config.transition_mins, + ), + Some(x) => x, + }); + } + + if let Some(plot) = &self.plot { + ui.separator(); + ui.add_space(SPACING); + + let first = plot.points.first().unwrap()[0]; + let last = plot.points.last().unwrap()[0]; + let line = Line::new(plot.points.clone()) + .name(LINE_NAME) + .highlight(true); + + Plot::new("brightness_curve") + .allow_drag(false) + .allow_zoom(false) + .allow_scroll(false) + .y_grid_spacer(uniform_grid_spacer(|_| [100.0, 20.0, 10.0])) + .y_axis_formatter(|val, _| format!("{}%", val)) + .x_grid_spacer(x_grid_spacer) + .label_formatter(|name, point| { + if name == LINE_NAME { + format!("{}\nBrightness {}%", convert_time(point.x), point.y) + } else { + String::new() + } + }) + .x_axis_formatter(|val, _| convert_time(val)) + .show(ui, |plot_ui| { + plot_ui.set_plot_bounds(PlotBounds::from_min_max([first, -5.0], [last, 105.0])); + plot_ui.line(line) + }); + } + } +} + +fn convert_time(time: f64) -> String { + let time = chrono::Local.timestamp_opt(time as i64, 0).unwrap(); + time.format("%I:%M %P").to_string() +} + +const HOURS: i64 = 6; + +// spaces the x-axis hourly +fn x_grid_spacer(input: GridInput) -> Vec { + let min_unix = input.bounds.0 as i64; + let max_unix = input.bounds.1 as i64; + let min_local = chrono::Local.timestamp_opt(min_unix, 0).unwrap(); + let lowest_whole_hour = min_local.duration_trunc(Duration::hours(HOURS)).unwrap(); + + let mut output = Vec::new(); + let hours_unix = HOURS * 3600; + + let mut rounded_unix = lowest_whole_hour.timestamp(); + while rounded_unix < max_unix { + if rounded_unix >= min_unix { + output.push(GridMark { + value: rounded_unix as f64, + step_size: hours_unix as f64, + }); + } + rounded_unix += hours_unix; + } + output +} + +fn generate_plot_data( + location: Location, + brightness_day: u32, + brightness_night: u32, + transition_mins: u32, +) -> PlotData { + log::debug!("Generating plot..."); + let timer_start = Instant::now(); + + let now = SystemTime::now(); + let graph_start = (now - Duration::hours(2).to_std().unwrap()) + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as i64; + let graph_end = (now + Duration::hours(22).to_std().unwrap()) + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as i64; + + let mut points = Vec::new(); + let mut current = graph_start; + + while current <= graph_end { + let sun = SunriseSunsetParameters::new(current, location.latitude, location.longitude) + .calculate() + .unwrap(); + let brightness = calculate_brightness( + brightness_day, + brightness_night, + transition_mins, + &sun, + current, + ); + let next_time = brightness.expiry_time.unwrap_or(graph_end).min(graph_end); + + // Add some extra points in the "flat" zone to allow cursor to snap to the line + // This is a bit of a hack, assuming if expiry is greater than 30 minutes, + // to be completely accurate we would need to look ahead at the next calculation. + if brightness.expiry_time.unwrap_or(i64::MAX) - current > 1800 { + for second in num::range_step(current, next_time, 240) { + points.push([second as f64, brightness.brightness as f64]); + } + } else { + points.push([current as f64, brightness.brightness as f64]); + } + + if current == graph_end { + break; + } + current = next_time; + } + + log::debug!( + "Plot took {:?} {} points", + timer_start.elapsed(), + points.len() + ); + + PlotData { + points, + generated_at: now, + brightness_day, + brightness_night, + transition_mins, + location, } } diff --git a/src/gui/status.rs b/src/gui/status.rs index cd9c13f..0f2319b 100644 --- a/src/gui/status.rs +++ b/src/gui/status.rs @@ -21,7 +21,7 @@ impl Page for StatusPage { } fn display_apply_results(results: &ApplyResults, ui: &mut egui::Ui) { - let date_format = "%H:%M %P (%b %d)"; + let date_format = "%I:%M %P (%b %d)"; let sunrise = Local .timestamp_opt(results.sun.rise, 0) .unwrap()