diff --git a/Cargo.lock b/Cargo.lock index 9cac320..d51dd9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2236,12 +2236,6 @@ dependencies = [ "libc", ] -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - [[package]] name = "memchr" version = "2.7.1" @@ -3404,7 +3398,7 @@ dependencies = [ [[package]] name = "solar-screen-brightness" -version = "2.0.0" +version = "2.1.0" dependencies = [ "anyhow", "brightness", @@ -3425,7 +3419,6 @@ dependencies = [ "image", "itertools 0.11.0", "log", - "maplit", "nix 0.22.3", "png", "pollster", diff --git a/Cargo.toml b/Cargo.toml index 0156d19..b220e09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "solar-screen-brightness" -version = "2.0.1" +version = "2.1.0" authors = ["Jacob Halsey "] edition = "2021" build = "build.rs" @@ -32,7 +32,6 @@ human-repr = "1.1.0" image = "0.24.7" itertools = "0.11.0" log = "0.4.14" -maplit = "1.0.2" png = "0.17.10" pollster = "0.3.0" serde = { version = "1.0.110", features = ["derive"] } diff --git a/screenshots/brightness.png b/screenshots/brightness.png index ae649eb..d8bdd46 100644 Binary files a/screenshots/brightness.png and b/screenshots/brightness.png differ diff --git a/screenshots/location.png b/screenshots/location.png index 72a1497..8b83da5 100644 Binary files a/screenshots/location.png and b/screenshots/location.png differ diff --git a/screenshots/status.png b/screenshots/status.png index 4eb10bb..ef9a1ed 100644 Binary files a/screenshots/status.png and b/screenshots/status.png differ diff --git a/src/apply.rs b/src/apply.rs index 0184d5e..7347652 100644 --- a/src/apply.rs +++ b/src/apply.rs @@ -1,9 +1,9 @@ use crate::calculator::calculate_brightness; -use crate::config::{BrightnessValues, Location, MonitorOverride}; +use crate::config::{BrightnessValues, Location, MonitorOverride, MonitorProperty}; use brightness::blocking::{Brightness, BrightnessDevice}; -use maplit::btreemap; +use itertools::Itertools; use serde::Serialize; -use std::collections::BTreeMap; +use std::collections::HashMap; use std::time::{SystemTime, UNIX_EPOCH}; use sunrise_sunset_calculator::SunriseSunsetParameters; use wildmatch::WildMatch; @@ -34,12 +34,22 @@ impl From for SunriseSunsetResul #[derive(Debug, Serialize)] pub struct MonitorResult { - pub device_name: String, - pub properties: BTreeMap<&'static str, String>, + pub properties: MonitorProperties, pub brightness: Option, pub error: Option, } +#[derive(Debug, Serialize)] +pub struct MonitorProperties { + pub device_name: String, + #[cfg(windows)] + pub device_description: String, + #[cfg(windows)] + pub device_key: String, + #[cfg(windows)] + pub device_path: String, +} + #[derive(Debug, Serialize)] pub struct BrightnessDetails { pub expiry_time: Option, @@ -50,7 +60,7 @@ pub struct BrightnessDetails { pub struct MonitorOverrideCompiled { pub pattern: WildMatch, - pub key: String, + pub key: MonitorProperty, pub brightness: Option, } @@ -58,8 +68,8 @@ impl From<&MonitorOverride> for MonitorOverrideCompiled { fn from(value: &MonitorOverride) -> Self { Self { pattern: WildMatch::new(&value.pattern), - key: value.key.clone(), - brightness: value.brightness.clone(), + key: value.key, + brightness: value.brightness, } } } @@ -67,10 +77,11 @@ impl From<&MonitorOverride> for MonitorOverrideCompiled { /// Find the first override that matches this monitor's properties fn match_monitor<'o>( overrides: &'o [MonitorOverrideCompiled], - monitor: &BTreeMap<&'static str, String>, + monitor: &MonitorProperties, ) -> Option<&'o MonitorOverrideCompiled> { + let map = monitor.to_map(); for o in overrides { - if let Some(value) = monitor.get(o.key.as_str()) { + if let Some(value) = map.get(&o.key) { if o.pattern.matches(value) { return Some(o); } @@ -108,14 +119,13 @@ pub fn apply_brightness( let monitor_results = monitors .into_iter() .map(|m| { - let device_name = m.device_name().unwrap_or_default(); - let properties = get_properties(&m).unwrap_or_default(); + let properties = MonitorProperties::from_device(&m); let monitor_values = match match_monitor(&overrides, &properties) { None => Some(BrightnessValues { brightness_day, brightness_night, }), - Some(o) => o.brightness.clone(), + Some(o) => o.brightness, }; if let Some(BrightnessValues { @@ -132,7 +142,7 @@ pub fn apply_brightness( ); log::debug!( "Computed brightness for '{}' = {:?} (day={}) (night={})", - device_name, + properties.device_name, brightness, brightness_day, brightness_night @@ -140,17 +150,20 @@ pub fn apply_brightness( let error = m.set(brightness.brightness).err(); if let Some(err) = error.as_ref() { - log::error!("Failed to set brightness for '{}': {:?}", device_name, err); + log::error!( + "Failed to set brightness for '{}': {:?}", + properties.device_name, + err + ); } else { log::info!( "Successfully set brightness for '{}' to {}%", - device_name, + properties.device_name, brightness.brightness ); } MonitorResult { - device_name, properties, brightness: Some(BrightnessDetails { expiry_time: brightness.expiry_time, @@ -161,15 +174,18 @@ pub fn apply_brightness( error: error.map(|e| e.to_string()), } } else { - log::info!("Skipping '{}' due to monitor override", device_name,); + log::info!( + "Skipping '{}' due to monitor override", + properties.device_name + ); MonitorResult { - device_name, properties, brightness: None, error: None, } } }) + .sorted_by_key(|m| m.properties.device_name.clone()) .collect::>(); ApplyResults { @@ -179,24 +195,30 @@ pub fn apply_brightness( } } -#[cfg(windows)] -pub fn get_properties( - device: &BrightnessDevice, -) -> Result, brightness::Error> { - use brightness::blocking::windows::BrightnessExt; - Ok(btreemap! { - "device_name" => device.device_name()?, - "device_description" => device.device_description()?, - "device_key" => device.device_registry_key()?, - "device_path" => device.device_path()?, - }) -} +impl MonitorProperties { + fn from_device(device: &BrightnessDevice) -> Self { + #[cfg(windows)] + use brightness::blocking::windows::BrightnessExt; + Self { + device_name: device.device_name().unwrap(), + #[cfg(windows)] + device_description: device.device_description().unwrap(), + #[cfg(windows)] + device_key: device.device_registry_key().unwrap(), + #[cfg(windows)] + device_path: device.device_path().unwrap(), + } + } -#[cfg(target_os = "linux")] -pub fn get_properties( - device: &BrightnessDevice, -) -> Result, brightness::Error> { - Ok(btreemap! { - "device_name" => device.device_name()?, - }) + pub fn to_map(&self) -> HashMap { + let mut map = HashMap::<_, &str>::new(); + map.insert(MonitorProperty::DeviceName, &self.device_name); + #[cfg(windows)] + { + map.insert(MonitorProperty::DeviceDescription, &self.device_description); + map.insert(MonitorProperty::DeviceKey, &self.device_key); + map.insert(MonitorProperty::DevicePath, &self.device_path); + } + map + } } diff --git a/src/config.rs b/src/config.rs index 1ab574c..372289c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,7 @@ //! SSB Config file definition - use crate::common::config_directory; use anyhow::Context; +use enum_iterator::Sequence; use serde::{Deserialize, Serialize}; use std::fs; use std::io::Write; @@ -34,15 +34,41 @@ pub struct SsbConfig { pub overrides: Vec, } +#[derive(Debug, Serialize, Deserialize, Copy, Clone, Eq, Hash, PartialEq, Sequence)] +#[serde(rename_all = "snake_case")] +pub enum MonitorProperty { + DeviceName, + #[cfg(windows)] + DeviceDescription, + #[cfg(windows)] + DeviceKey, + #[cfg(windows)] + DevicePath, +} + +impl MonitorProperty { + pub fn as_str(&self) -> &'static str { + match self { + MonitorProperty::DeviceName => "Name", + #[cfg(windows)] + MonitorProperty::DeviceDescription => "Description", + #[cfg(windows)] + MonitorProperty::DeviceKey => "Key", + #[cfg(windows)] + MonitorProperty::DevicePath => "Path", + } + } +} + #[derive(Debug, Deserialize, Serialize, Validate, Clone)] pub struct MonitorOverride { pub pattern: String, - pub key: String, + pub key: MonitorProperty, #[validate] pub brightness: Option, } -#[derive(Debug, Serialize, Deserialize, Validate, Clone)] +#[derive(Debug, Serialize, Deserialize, Validate, Copy, Clone)] pub struct BrightnessValues { #[validate(range(max = 100))] pub brightness_day: u32, diff --git a/src/gui/app.rs b/src/gui/app.rs index 8c62489..2b93881 100644 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -4,6 +4,7 @@ use crate::controller::Message; use crate::gui::brightness_settings::BrightnessSettingsPage; use crate::gui::help::HelpPage; use crate::gui::location_settings::LocationSettingsPage; +use crate::gui::monitor_overrides::MonitorOverridePage; use crate::gui::status::StatusPage; use crate::gui::UserEvent; use egui::{Align, Color32, Layout, ScrollArea}; @@ -16,7 +17,7 @@ use std::sync::{Arc, Mutex, RwLock}; pub const SPACING: f32 = 10.0; pub trait Page { - fn render(&mut self, ui: &mut egui::Ui, context: &mut AppState); + fn render(&mut self, ui: &mut egui::Ui, app_state: &mut AppState); } pub struct SsbEguiApp { @@ -24,6 +25,7 @@ pub struct SsbEguiApp { brightness_settings_page: BrightnessSettingsPage, pub location_settings_page: LocationSettingsPage, help_page: HelpPage, + monitor_override_page: MonitorOverridePage, context: AppState, pub modal: Option>, } @@ -55,6 +57,7 @@ enum PageId { Status, BrightnessSettings, LocationSettings, + MonitorOverrides, Help, } @@ -65,6 +68,7 @@ impl PageId { PageId::BrightnessSettings => "Brightness Settings", PageId::LocationSettings => "Location Settings", PageId::Help => "Help", + PageId::MonitorOverrides => "Monitor Overrides", } } @@ -74,6 +78,7 @@ impl PageId { PageId::BrightnessSettings => "🔅", PageId::LocationSettings => "🌐", PageId::Help => "❔", + PageId::MonitorOverrides => "💻", } } } @@ -143,6 +148,7 @@ impl SsbEguiApp { selected_page: PageId::Status, brightness_settings_page: BrightnessSettingsPage::from_config(&config_read), location_settings_page: LocationSettingsPage::from_config(&config_read), + monitor_override_page: MonitorOverridePage::from_config(&config_read), modal: None, context: AppState { main_loop, @@ -221,7 +227,28 @@ impl SsbEguiApp { self.location_settings_page.render(ui, &mut self.context) } PageId::Help => self.help_page.render(ui, &mut self.context), + PageId::MonitorOverrides => { + self.monitor_override_page.render(ui, &mut self.context) + } } }); } } + +pub fn set_red_widget_border(ui: &mut egui::Ui) { + ui.style_mut().visuals.widgets.inactive.bg_stroke.color = egui::Color32::RED; + ui.style_mut().visuals.widgets.inactive.bg_stroke.width = 1.0; + ui.style_mut().visuals.widgets.hovered.bg_stroke.color = egui::Color32::RED; +} + +pub fn save_config(config: &mut SsbConfig, transitions: &Transitions) { + if let Err(e) = config.save() { + log::error!("Unable to save config: {:#}", e); + transitions.queue_state_transition(move |app| { + app.modal = Some(Box::new(MessageModal { + title: "Error".to_string(), + message: format!("Unable to save config: {}", e), + })); + }); + } +} diff --git a/src/gui/brightness_settings.rs b/src/gui/brightness_settings.rs index 438ce44..ecc801f 100644 --- a/src/gui/brightness_settings.rs +++ b/src/gui/brightness_settings.rs @@ -1,6 +1,6 @@ use crate::config::SsbConfig; use crate::controller::Message; -use crate::gui::app::{AppState, MessageModal, Page, SPACING}; +use crate::gui::app::{save_config, AppState, Page, SPACING}; use validator::Validate; pub struct BrightnessSettingsPage { @@ -52,7 +52,7 @@ impl Page for BrightnessSettingsPage { self.copy_to_config(&mut config); app_state .controller - .send(Message::Refresh("Setting change")) + .send(Message::Refresh("Brightness change")) .unwrap(); } if ui.button("Save").clicked() { @@ -60,17 +60,9 @@ impl Page for BrightnessSettingsPage { self.copy_to_config(&mut config); app_state .controller - .send(Message::Refresh("Setting change")) + .send(Message::Refresh("Brightness change")) .unwrap(); - if let Err(e) = config.save() { - log::error!("Unable to save config: {:#}", e); - app_state.transitions.queue_state_transition(move |app| { - app.modal = Some(Box::new(MessageModal { - title: "Error".to_string(), - message: format!("Unable to save config: {}", e), - })); - }); - } + save_config(&mut config, &app_state.transitions); }; }); } diff --git a/src/gui/location_settings.rs b/src/gui/location_settings.rs index a9955f1..a1fdbd0 100644 --- a/src/gui/location_settings.rs +++ b/src/gui/location_settings.rs @@ -1,6 +1,8 @@ use crate::config::{Location, SsbConfig}; use crate::controller::Message; -use crate::gui::app::{AppState, MessageModal, Modal, Page, SPACING}; +use crate::gui::app::{ + save_config, set_red_widget_border, AppState, MessageModal, Modal, Page, SPACING, +}; use crate::gui::UserEvent; use egui::{Context, Widget}; use geocoding::{Forward, GeocodingError, Openstreetmap}; @@ -99,27 +101,13 @@ impl Page for LocationSettingsPage { self.copy_to_config(&mut config); app_state .controller - .send(Message::Refresh("Setting change")) + .send(Message::Refresh("Location change")) .unwrap(); - if let Err(e) = config.save() { - log::error!("Unable to save config: {:#}", e); - app_state.transitions.queue_state_transition(move |app| { - app.modal = Some(Box::new(MessageModal { - title: "Error".to_string(), - message: format!("Unable to save config: {}", e), - })); - }); - } + save_config(&mut config, &app_state.transitions); } } } -fn set_red_widget_border(ui: &mut egui::Ui) { - ui.style_mut().visuals.widgets.inactive.bg_stroke.color = egui::Color32::RED; - ui.style_mut().visuals.widgets.inactive.bg_stroke.width = 1.0; - ui.style_mut().visuals.widgets.hovered.bg_stroke.color = egui::Color32::RED; -} - fn coord_to_string(coord: f64) -> String { format!("{:.5}", coord) } diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 64303f2..a8eb9e0 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -2,6 +2,7 @@ pub mod app; mod brightness_settings; mod help; mod location_settings; +mod monitor_overrides; mod status; use crate::common::APP_NAME; diff --git a/src/gui/monitor_overrides.rs b/src/gui/monitor_overrides.rs new file mode 100644 index 0000000..5717a36 --- /dev/null +++ b/src/gui/monitor_overrides.rs @@ -0,0 +1,247 @@ +use crate::apply::ApplyResults; +use crate::config::{BrightnessValues, MonitorOverride, MonitorProperty, SsbConfig}; +use crate::controller::Message; +use crate::gui::app::{save_config, set_red_widget_border, AppState, Page, SPACING}; +use crate::gui::status::no_devices_found; +use ellipse::Ellipse; + +const MAX_OVERRIDES: usize = 10; + +pub struct MonitorOverridePage { + overrides: Vec, +} + +struct Override { + key: MonitorProperty, + pattern: String, + disable: bool, + day: u32, + night: u32, +} + +impl Override { + fn is_valid(&self) -> bool { + !self.pattern.is_empty() + } +} + +impl Page for MonitorOverridePage { + fn render(&mut self, ui: &mut egui::Ui, app_state: &mut AppState) { + { + let results = app_state.results.read().unwrap(); + if let Some(results) = results.as_ref() { + self.render_monitors(ui, results); + ui.add_space(SPACING); + ui.separator(); + } + } + ui.add_space(SPACING); + self.render_overrides(ui, app_state); + } +} + +impl MonitorOverridePage { + pub fn from_config(config: &SsbConfig) -> Self { + let overrides = config + .overrides + .iter() + .map(|o| Override { + key: o.key, + pattern: o.pattern.clone(), + disable: o.brightness.is_none(), + day: o.brightness.map(|b| b.brightness_day).unwrap_or(100), + night: o.brightness.map(|b| b.brightness_night).unwrap_or(60), + }) + .collect(); + Self { overrides } + } + + fn copy_to_config(&self, config: &mut SsbConfig) { + config.overrides = self + .overrides + .iter() + .map(|o| MonitorOverride { + pattern: o.pattern.clone(), + key: o.key, + brightness: (!o.disable).then_some(BrightnessValues { + brightness_day: o.day, + brightness_night: o.night, + }), + }) + .collect(); + } + + fn is_valid(&self) -> bool { + self.overrides.iter().all(|o| o.is_valid()) + } + + fn render_monitors(&mut self, ui: &mut egui::Ui, results: &ApplyResults) { + ui.label(egui::RichText::new("Monitor Properties").size(14.0)); + ui.add_space(SPACING); + + if results.monitors.is_empty() { + ui.add(no_devices_found()); + return; + } + + let properties = enum_iterator::all::().collect::>(); + + egui::ScrollArea::horizontal().show(ui, |ui| { + egui::Grid::new("monitors_properties_grid") + .striped(true) + .num_columns(properties.len()) + .show(ui, |ui| { + // Header row + for p in &properties { + ui.label(p.as_str()); + } + ui.end_row(); + // Monitor rows + for monitor in &results.monitors { + let property_map = monitor.properties.to_map(); + for property in &properties { + if let Some(value) = property_map.get(property) { + let value = value.to_string(); + let button = egui::Button::new(value.as_str().truncate_ellipse(24)) + .frame(false); + let hover = |ui: &mut egui::Ui| { + ui.label(format!("\"{}\"", value)); + ui.label("Click to copy 📋"); + }; + if ui.add(button).on_hover_ui(hover).clicked() { + ui.output_mut(|o| o.copied_text = value); + } + } else { + ui.label(""); + } + } + ui.end_row(); + } + }); + }); + } + + fn render_overrides(&mut self, ui: &mut egui::Ui, app_state: &mut AppState) { + ui.label(egui::RichText::new("Overrides").size(14.0)); + ui.add_space(SPACING); + ui.label("Create monitor overrides that match one of the above monitor properties."); + ui.label("The first override that is matched will be applied to the monitor."); + ui.add_space(SPACING); + + let properties = enum_iterator::all::().collect::>(); + + if !self.overrides.is_empty() { + egui::Grid::new("overrides_grid") + .striped(true) + .num_columns(8) + .min_col_width(0.0) + .show(ui, |ui| { + ui.label(""); + ui.label(""); + ui.label("Property"); + ui.label("Pattern") + .on_hover_text("You can use * as a wildcard match"); + ui.label("Disable") + .on_hover_text("Disable automatic brightness"); + ui.label("Day"); + ui.label("Night"); + ui.label(""); + ui.end_row(); + + let last_idx = self.overrides.len() - 1; + for idx in 0..self.overrides.len() { + if ui + .add_enabled(idx != 0, egui::Button::new("⬆")) + .on_hover_text("Move up") + .clicked() + { + self.overrides.swap(idx, idx - 1); + } + if ui + .add_enabled(idx != last_idx, egui::Button::new("⬇")) + .on_hover_text("Move down") + .clicked() + { + self.overrides.swap(idx, idx + 1); + } + let o = self.overrides.get_mut(idx).unwrap(); + egui::ComboBox::from_id_source(format!("override_key {}", idx)) + .selected_text(o.key.as_str()) + .show_ui(ui, |ui| { + for property in &properties { + ui.selectable_value(&mut o.key, *property, property.as_str()); + } + }); + + ui.add_enabled_ui(true, |ui| { + if o.pattern.is_empty() { + set_red_widget_border(ui); + } + ui.add( + egui::TextEdit::singleline(&mut o.pattern) + .min_size(egui::vec2(140.0, 0.0)), + ); + }); + + ui.add(egui::Checkbox::without_text(&mut o.disable)); + + if o.disable { + ui.label("N/A"); + ui.label("N/A"); + } else { + ui.add( + egui::DragValue::new(&mut o.day) + .clamp_range(0u32..=100u32) + .suffix("%"), + ); + ui.add( + egui::DragValue::new(&mut o.night) + .clamp_range(0u32..=100u32) + .suffix("%"), + ); + } + + if ui.button("❌").on_hover_text("Remove override").clicked() { + self.overrides.remove(idx); + return; // Important to avoid invalid index on next iteration + }; + + ui.end_row(); + } + }); + ui.add_space(SPACING); + } + + ui.horizontal(|ui| { + if ui + .add_enabled( + self.overrides.len() < MAX_OVERRIDES, + egui::Button::new("Create new override"), + ) + .clicked() + { + self.overrides.push(Override { + key: MonitorProperty::DeviceName, + pattern: "".to_string(), + disable: false, + day: 100, + night: 60, + }) + } + if ui + .add_enabled(self.is_valid(), egui::Button::new("Save")) + .clicked() + { + let mut config = app_state.config.write().unwrap(); + self.copy_to_config(&mut config); + app_state + .controller + .send(Message::Refresh("Override change")) + .unwrap(); + save_config(&mut config, &app_state.transitions); + } + }); + + ui.add_space(SPACING); + } +} diff --git a/src/gui/status.rs b/src/gui/status.rs index 8acfdd3..cd9c13f 100644 --- a/src/gui/status.rs +++ b/src/gui/status.rs @@ -1,7 +1,6 @@ use crate::apply::ApplyResults; use crate::gui::app::{AppState, Page, SPACING}; use chrono::{Local, TimeZone}; -use itertools::Itertools; pub struct StatusPage; @@ -59,63 +58,63 @@ fn display_apply_results(results: &ApplyResults, ui: &mut egui::Ui) { ui.add_space(SPACING); if results.monitors.is_empty() { - ui.label(egui::RichText::new("No devices found").color(egui::Color32::RED)); - } else { - egui::Grid::new("monitors_grid") - .striped(true) - .num_columns(5) - .show(ui, |ui| { - ui.label("Device Name"); - ui.label("Day") - .on_hover_text("Configured day time brightness"); - ui.label("Night") - .on_hover_text("Configured night time brightness"); - ui.label("Now") - .on_hover_text("The computed brightness percentage for this monitor"); - ui.label("Status"); - ui.label("Next update") - .on_hover_text("Time that the brightness will be changed"); - ui.end_row(); + ui.add(no_devices_found()); + return; + } - results - .monitors - .iter() - .sorted_by_key(|m| &m.device_name) - .for_each(|monitor| { - ui.label(&monitor.device_name); - if let Some(brightness) = &monitor.brightness { - ui.label(format!("{}%", brightness.brightness_day)); - ui.label(format!("{}%", brightness.brightness_night)); - ui.label(format!("{}%", brightness.brightness)); + egui::Grid::new("monitors_grid") + .striped(true) + .num_columns(5) + .show(ui, |ui| { + ui.label("Name"); + ui.label("Day") + .on_hover_text("Configured day time brightness"); + ui.label("Night") + .on_hover_text("Configured night time brightness"); + ui.label("Now") + .on_hover_text("The computed brightness percentage for this monitor"); + ui.label("Status"); + ui.label("Next update") + .on_hover_text("Time that the brightness will be changed"); + ui.end_row(); - match &monitor.error { - None => ui - .label("Ok") - .on_hover_text("Brightness was applied successfully"), - Some(e) => ui - .label(egui::RichText::new("Error").color(egui::Color32::RED)) - .on_hover_text(e), - }; + results.monitors.iter().for_each(|monitor| { + ui.label(&monitor.properties.device_name); + if let Some(brightness) = &monitor.brightness { + ui.label(format!("{}%", brightness.brightness_day)); + ui.label(format!("{}%", brightness.brightness_night)); + ui.label(format!("{}%", brightness.brightness)); - match brightness.expiry_time { - None => ui.label("Never"), - Some(expiry_time) => { - let changes_at = Local.timestamp_opt(expiry_time, 0).unwrap(); - ui.label(changes_at.format("%H:%M %P").to_string()) - .on_hover_text(changes_at.format("%b %d").to_string()) - } - }; - } else { - (0..3).for_each(|_| { - ui.label("N/A"); - }); - ui.label("Disabled").on_hover_text( - "Dynamic brightness is disabled due to a monitor override", - ); - ui.label("Never"); + match &monitor.error { + None => ui + .label("Ok") + .on_hover_text("Brightness was applied successfully"), + Some(e) => ui + .label(egui::RichText::new("Error").color(egui::Color32::RED)) + .on_hover_text(e), + }; + + match brightness.expiry_time { + None => ui.label("Never"), + Some(expiry_time) => { + let changes_at = Local.timestamp_opt(expiry_time, 0).unwrap(); + ui.label(changes_at.format("%H:%M %P").to_string()) + .on_hover_text(changes_at.format("%b %d").to_string()) } - ui.end_row(); + }; + } else { + (0..3).for_each(|_| { + ui.label("N/A"); }); + ui.label("Disabled") + .on_hover_text("Dynamic brightness is disabled due to a monitor override"); + ui.label("Never"); + } + ui.end_row(); }); - } + }); +} + +pub fn no_devices_found() -> egui::Label { + egui::Label::new(egui::RichText::new("No devices found").color(egui::Color32::RED)) }