From df00da3b0fd720867f75f99a9ead97a71843657d Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 30 May 2024 17:26:47 +0200 Subject: [PATCH 1/8] update --- Cargo.toml | 12 +-- src/config.rs | 72 ++--------------- src/customapp.rs | 140 --------------------------------- src/main.rs | 3 +- src/ui/custom.rs | 184 -------------------------------------------- src/ui/device.rs | 39 ++++------ src/ui/mod.rs | 52 ++++++------- src/ui/screen.rs | 117 ++++++++++++++-------------- src/ui/settings.rs | 68 +++++++--------- src/ui/status.rs | 65 ++++++---------- src/ui/statusbar.rs | 19 ++--- 11 files changed, 171 insertions(+), 600 deletions(-) delete mode 100644 src/customapp.rs delete mode 100644 src/ui/custom.rs diff --git a/Cargo.toml b/Cargo.toml index 99e6d76..b8e93a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,18 +25,18 @@ short_description = "A GUI for the awtrix clock." [dependencies] # Error handling -anyhow = "1.0.83" +anyhow = "1.0.86" # Networking reqwest = { version = "0.12.4", features = ["blocking"] } # Parsing -serde = { version = "1.0.200", features = ["derive"] } -serde_json = "1.0.116" -semver = "1.0.22" +serde = { version = "1.0.203", features = ["derive"] } +serde_json = "1.0.117" +semver = "1.0.23" # GUI eframe = { version = "0.27.2", features = ["wgpu"] } egui = "0.27.2" egui-notify = "0.14.0" egui_extras = { version = "0.27.2", features = ["syntect", "image"] } image = "0.25.1" -open = "5.1.2" -ping = "0.5.2" +open = "5.1.3" +parking_lot = "0.12.3" diff --git a/src/config.rs b/src/config.rs index d9afbe3..2fe52ae 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,23 +1,13 @@ use anyhow::Context; use core::str; -use ping::dgramsock::ping; use serde::{Deserialize, Serialize}; -use std::{ - env, fs, - io::Write, - net::IpAddr, - str::FromStr, - sync::mpsc, - thread, - time::{Duration, Instant}, -}; +use std::{env, fs, io::Write, time::Instant}; const ENV: &str = ".awtrix.env"; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Config { - pub ip: Option, - pub ip_str: String, + pub ip: String, #[serde(skip, default = "Instant::now")] pub last_ping: Instant, // true = On, false = Off @@ -27,38 +17,15 @@ pub struct Config { impl Config { pub fn new() -> Self { match Self::read() { - Ok(config) => { - let mut config = config; - config.check_status(true); - config - } + Ok(config) => config, Err(_) => Self { - ip: None, - ip_str: String::new(), + ip: String::new(), last_ping: Instant::now(), last_state: false, }, } } - pub fn check_status(&mut self, force: bool) { - if !force && self.last_ping.elapsed() < Duration::from_secs(10) { - return; - } - - if let Some(ip) = self.ip { - self.last_state = Config::check_power(ip).is_ok(); - self.last_ping = Instant::now(); - } - } - - pub fn set_ip(&mut self) -> anyhow::Result<()> { - let ip = IpAddr::from_str(&self.ip_str).context("Failed to parse IP")?; - self.ip = Some(ip); - Config::write(self)?; - Ok(()) - } - fn read() -> anyhow::Result { let curr = env::current_exe()?; let filepath = curr.parent().context("Failed to gt parent path")?.join(ENV); @@ -67,41 +34,14 @@ impl Config { serde_json::from_str(&content).context("Failed to deserialize Config") } - pub fn write(config: &Config) -> anyhow::Result<()> { + pub fn write(&self) -> anyhow::Result<()> { if let Some(filepath) = env::current_exe()?.parent().map(|x| x.join(ENV)) { let mut file = fs::File::create(filepath)?; - file.write_all(serde_json::to_string(config)?.as_bytes())?; + file.write_all(serde_json::to_string(self)?.as_bytes())?; file.flush()?; Ok(()) } else { anyhow::bail!("Failed to write to file") } } - - fn check_power(ip: IpAddr) -> anyhow::Result<()> { - check_ip(ip)?; - - let (sender, receiver) = mpsc::channel(); - thread::spawn(move || { - sender.send(ping( - ip, - Some(Duration::from_secs(1)), - None, - None, - None, - None, - )) - }); - match receiver.recv()? { - Ok(()) => Ok(()), - _ => anyhow::bail!("Device is not reachable"), - } - } -} - -pub fn check_ip(ip: IpAddr) -> anyhow::Result<()> { - if ip.is_unspecified() { - anyhow::bail!("IP is empty"); - } - Ok(()) } diff --git a/src/customapp.rs b/src/customapp.rs deleted file mode 100644 index 6ff2746..0000000 --- a/src/customapp.rs +++ /dev/null @@ -1,140 +0,0 @@ -use egui::Color32; -use serde::{Deserialize, Serialize, Serializer}; -//use struct_iterable::Iterable; - -#[derive(Serialize, Deserialize, Debug)] -pub struct CustomApp { - #[serde(skip_serializing_if = "String::is_empty")] - pub text: String, - #[serde(rename = "textCase", skip_serializing_if = "Option::is_none")] - pub text_case: Option, - #[serde(rename = "topText", skip_serializing_if = "Option::is_none")] - pub top_text: Option, - #[serde(rename = "textOffset", skip_serializing_if = "Option::is_none")] - pub text_offset: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub center: Option, - #[serde( - skip_serializing_if = "Option::is_none", - serialize_with = "serialize_to_i32" - )] - pub color: Option, - #[serde( - skip_serializing_if = "Option::is_none", - serialize_with = "serialize_to_i32" - )] - pub gradient: Option, - #[serde(rename = "blinkText", skip_serializing_if = "Option::is_none")] - pub blink_text: Option, - #[serde(rename = "fadeText", skip_serializing_if = "Option::is_none")] - pub fade_text: Option, - #[serde( - skip_serializing_if = "Option::is_none", - serialize_with = "serialize_to_i32" - )] - pub background: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub rainbow: Option, - #[serde(skip_serializing_if = "String::is_empty")] - pub icon: String, - #[serde(rename = "pushIcon", skip_serializing_if = "Option::is_none")] - pub push_icon: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub repeat: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub duration: Option, - #[serde( - skip_serializing_if = "Option::is_none", - serialize_with = "serialize_to_i32" - )] - pub bar: Option, - #[serde( - skip_serializing_if = "Option::is_none", - serialize_with = "serialize_to_i32" - )] - pub line: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub autoscale: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub progress: Option, - #[serde( - rename = "progressC", - skip_serializing_if = "Option::is_none", - serialize_with = "serialize_to_i32" - )] - pub progress_c: Option, - #[serde( - rename = "progressBC", - skip_serializing_if = "Option::is_none", - serialize_with = "serialize_to_i32" - )] - pub progress_bc: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub pos: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub lifetime: Option, - #[serde(rename = "lifetimeMode", skip_serializing_if = "Option::is_none")] - pub lifetime_mode: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub no_scroll: Option, - #[serde(rename = "scrollSpeed", skip_serializing_if = "Option::is_none")] - pub scroll_speed: Option, - #[serde(skip_serializing_if = "String::is_empty")] - pub effect: String, - #[serde(skip_serializing_if = "String::is_empty")] - pub overlay: String, -} - -#[allow(clippy::trivially_copy_pass_by_ref)] -fn serialize_to_i32(c: &Option, serializer: S) -> Result -where - S: Serializer, -{ - if let Some(color) = c { - let red = i32::from(color.r()) << 16; - let green = i32::from(color.g()) << 8; - let blue = i32::from(color.b()); - serializer.serialize_i32(red | green | blue) - } else { - serializer.serialize_none() - } -} - -impl CustomApp { - pub fn new() -> Self { - Self { - text: String::new(), - text_case: None, - top_text: None, - text_offset: None, - center: None, - color: None, - gradient: None, - blink_text: None, - fade_text: None, - background: None, - rainbow: None, - icon: String::new(), - push_icon: None, - repeat: None, - duration: None, - bar: None, - line: None, - autoscale: None, - progress: None, - progress_c: None, - progress_bc: None, - pos: None, - lifetime: None, - lifetime_mode: None, - no_scroll: None, - scroll_speed: None, - effect: String::new(), - overlay: String::new(), - } - } - - pub fn to_json(&self) -> anyhow::Result { - serde_json::to_string(self).map_err(|e| anyhow::anyhow!(e.to_string())) - } -} diff --git a/src/main.rs b/src/main.rs index 270da79..9cbb2cd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ #![warn(clippy::perf)] #![warn(clippy::style)] #![deny(clippy::all)] -#![deny(clippy::unwrap_used)] +#![warn(clippy::unwrap_used)] #![deny(clippy::expect_used)] #![allow(clippy::cast_precision_loss)] #![allow(clippy::cast_possible_truncation)] @@ -11,7 +11,6 @@ use anyhow::Context; use egui::ViewportBuilder; mod config; -mod customapp; mod ui; fn main() -> anyhow::Result<()> { diff --git a/src/ui/custom.rs b/src/ui/custom.rs deleted file mode 100644 index 5f6c924..0000000 --- a/src/ui/custom.rs +++ /dev/null @@ -1,184 +0,0 @@ -use std::time::Duration; - -use egui::{vec2, Align, Align2, Button, Color32, Frame, Layout, ScrollArea, TextEdit, Ui, Window}; - -use egui_notify::Toasts; - -use crate::customapp::CustomApp; - -pub struct Custom { - toasts: Toasts, - app: CustomApp, - show_preview: bool, -} - -impl Custom { - pub fn new() -> Self { - Self { - toasts: Toasts::new().with_anchor(egui_notify::Anchor::BottomLeft), - app: CustomApp::new(), - show_preview: false, - } - } - - pub fn show(&mut self, ui: &mut Ui) { - ui.horizontal(|ui| { - ui.add(Button::new("Set App")) - .clicked() - .then(|| match self.app.to_json() { - Ok(json) => { - self.toasts - .info("Not implemented yet") - .set_duration(Some(Duration::from_secs(5))); - println!("{json}"); - } - Err(e) => { - self.toasts - .info(format!("Error {e}")) - .set_duration(Some(Duration::from_secs(5))); - println!("{e}"); - } - }); - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - ui.add(Button::new("Reset")).clicked().then(|| { - self.app = CustomApp::new(); - }); - ui.add(Button::new("Preview")) - .clicked() - .then(|| self.show_preview = true); - }); - }); - ui.separator(); - //ScrollArea::new([false, true]).show(ui, |ui| { - // for (name, value) in self.custom_app.iter() { - // // Change the type of value to &mut dyn Any - // ui.horizontal(|ui| { - // ui.label(name); - // if value.is::>() { - // ui.label("bool"); - // } else if value.is::>() { - // ui.label("f32"); - // } else if value.is::>() { - // ui.label("i32"); - // } else if value.is::>>() { - // ui.label("Option>"); - // } else if value.is::>() { - // Self::show_text_edit(ui, self.custom_app.get_mut(&name)); - // } else { - // ui.label("unknown"); - // } - // }); - // } - //}); - ScrollArea::new([false, true]) - .max_width(f32::INFINITY) - .show(ui, |ui| { - Self::show_text_edit(ui, &mut self.app.text, "Text"); - Self::show_int_edit(ui, &mut self.app.text_case, "Text Case"); - Self::show_bool_edit(ui, &mut self.app.top_text, "Top Text"); - Self::show_int_edit(ui, &mut self.app.text_offset, "Text Offset"); - Self::show_bool_edit(ui, &mut self.app.center, "Center"); - Self::show_color_edit(ui, &mut self.app.color, "Color"); - Self::show_color_edit(ui, &mut self.app.gradient, "Gradient"); - Self::show_int_edit(ui, &mut self.app.blink_text, "Blink Text"); - Self::show_int_edit(ui, &mut self.app.fade_text, "Fade Text"); - Self::show_color_edit(ui, &mut self.app.background, "Background"); - Self::show_bool_edit(ui, &mut self.app.rainbow, "Rainbow"); - Self::show_text_edit(ui, &mut self.app.icon, "Icon"); - Self::show_int_edit(ui, &mut self.app.push_icon, "Push Icon"); - Self::show_int_edit(ui, &mut self.app.repeat, "Repeat"); - Self::show_int_edit(ui, &mut self.app.duration, "Duration"); - Self::show_color_edit(ui, &mut self.app.bar, "Bar"); - Self::show_color_edit(ui, &mut self.app.line, "Line"); - Self::show_bool_edit(ui, &mut self.app.autoscale, "Autoscale"); - Self::show_int_edit(ui, &mut self.app.progress, "Progress"); - Self::show_color_edit(ui, &mut self.app.progress_c, "Progress C"); - Self::show_color_edit(ui, &mut self.app.progress_bc, "Progress BC"); - Self::show_int_edit(ui, &mut self.app.pos, "Pos"); - Self::show_int_edit(ui, &mut self.app.lifetime, "Lifetime"); - Self::show_int_edit(ui, &mut self.app.lifetime_mode, "Lifetime Mode"); - Self::show_bool_edit(ui, &mut self.app.no_scroll, "No Scroll"); - Self::show_int_edit(ui, &mut self.app.scroll_speed, "Scroll Speed"); - Self::show_text_edit(ui, &mut self.app.effect, "Effect"); - Self::show_text_edit(ui, &mut self.app.overlay, "Overlay"); - }); - - self.preview_window(ui); - self.toasts.show(ui.ctx()); - } - - fn show_color_edit(ui: &mut Ui, color: &mut Option, name: &str) { - ui.horizontal(|ui| { - ui.label(name); - if let Some(existing_color) = color { - ui.color_edit_button_srgba(existing_color); - } else { - let mut empty_color = Color32::WHITE; - ui.color_edit_button_srgba(&mut empty_color); - if empty_color != Color32::WHITE { - *color = Some(empty_color); - } - } - }); - } - - fn show_bool_edit(ui: &mut Ui, value: &mut Option, name: &str) { - ui.horizontal(|ui| { - ui.label(name); - if let Some(existing_value) = value { - ui.checkbox(existing_value, ""); - } else { - let mut empty_value = false; - ui.checkbox(&mut empty_value, ""); - if empty_value { - *value = Some(empty_value); - } - } - }); - } - - fn show_int_edit(ui: &mut Ui, value: &mut Option, name: &str) { - ui.horizontal(|ui| { - ui.label(name); - if let Some(existing_value) = value { - ui.add(egui::DragValue::new(existing_value)); - } else { - let mut empty_value = 0; - ui.add(egui::DragValue::new(&mut empty_value)); - if empty_value != 0 { - *value = Some(empty_value); - } - } - }); - } - - fn show_text_edit(ui: &mut Ui, text: &mut String, name: &str) { - ui.horizontal(|ui| { - ui.label(name); - ui.add(TextEdit::singleline(text).desired_width(150.0)); - }); - } - - fn preview_window(&mut self, ui: &mut Ui) { - let string = match self.app.to_json() { - Ok(string) => string.replace(',', ",\n"), - Err(_) => "Failed to serialize".to_string(), - }; - Window::new("Preview") - .resizable(false) - .collapsible(false) - .open(&mut self.show_preview) - .anchor(Align2::CENTER_CENTER, (0.0, 0.0)) - .fixed_size(vec2(200.0, 150.0)) - .frame(Frame::window(ui.style()).fill(ui.style().visuals.widgets.open.weak_bg_fill)) - .show(ui.ctx(), |ui| { - ui.vertical_centered(|ui| { - ScrollArea::new([false, true]) - .max_width(f32::INFINITY) - .show(ui, |ui| { - ui.label(string); - }); - }); - }); - } -} diff --git a/src/ui/device.rs b/src/ui/device.rs index 42facc5..8021323 100644 --- a/src/ui/device.rs +++ b/src/ui/device.rs @@ -1,4 +1,4 @@ -use std::{net::IpAddr, time::Duration}; +use std::time::Duration; use anyhow::Context; use egui::{Button, DragValue, Label, ScrollArea, SidePanel, Ui}; @@ -7,8 +7,6 @@ use reqwest::blocking::Client; use semver::Version; use serde_json::{from_str, Value}; -use crate::config; - use super::status::{self, Stat}; pub struct Device { @@ -29,7 +27,7 @@ impl Device { pub fn show( &mut self, ui: &mut Ui, - ip: Option, + ip: &str, stats: &Option, write_boot: &mut (bool, bool), ) { @@ -41,25 +39,26 @@ impl Device { ui.heading("Awtrix Options"); }); ui.separator(); - if let Some(ip) = ip { + if ip.is_empty() { + ui.label("No IP set"); + } else { ui.vertical_centered(|ui| { ui.horizontal(|ui| { - self.power(ui, ip); + // TODO: FIX THIS + //self.power(ui, ip); self.reboot(ui, ip, write_boot); ui.separator(); self.sleep(ui, ip); }); }); self.update_device(ui, ip, stats); - } else { - ui.label("No IP set"); } }); }); self.toasts.show(ui.ctx()); } - fn power(&mut self, ui: &mut Ui, ip: IpAddr) { + fn power(&mut self, ui: &mut Ui, ip: &str) { fn handle_power_result(result: &anyhow::Result<()>, toasts: &mut Toasts, power: &str) { match result { Ok(()) => toasts @@ -81,8 +80,7 @@ impl Device { }); } - fn set_power(ip: IpAddr, curr_power: bool) -> anyhow::Result<()> { - config::check_ip(ip)?; + fn set_power(ip: &str, curr_power: bool) -> anyhow::Result<()> { let payload = format!("{{\"power\": {curr_power}}}"); Client::new() .post(format!("http://{ip}/api/power")) @@ -95,7 +93,7 @@ impl Device { .context("Failed to set power") } - fn sleep(&mut self, ui: &mut Ui, ip: IpAddr) { + fn sleep(&mut self, ui: &mut Ui, ip: &str) { ui.horizontal(|ui| { ui.add( DragValue::new(&mut self.time) @@ -118,8 +116,7 @@ impl Device { }); } - fn set_sleep(&self, ip: IpAddr) -> anyhow::Result<()> { - config::check_ip(ip)?; + fn set_sleep(&self, ip: &str) -> anyhow::Result<()> { let payload = format!("{{\"sleep\": {}}}", self.time); Client::new() .post(format!("http://{ip}/api/sleep")) @@ -131,7 +128,7 @@ impl Device { .context("Failed to set sleep") } - fn reboot(&mut self, ui: &mut Ui, ip: IpAddr, write_boot: &mut (bool, bool)) { + fn reboot(&mut self, ui: &mut Ui, ip: &str, write_boot: &mut (bool, bool)) { ui.button("Reboot") .clicked() .then(|| match Self::set_reboot(ip) { @@ -149,8 +146,7 @@ impl Device { }); } - fn set_reboot(ip: IpAddr) -> anyhow::Result<()> { - config::check_ip(ip)?; + fn set_reboot(ip: &str) -> anyhow::Result<()> { Client::new() .post(format!("http://{ip}/api/reboot")) .body("-") @@ -161,7 +157,7 @@ impl Device { .context("Failed to reboot") } - fn update_device(&mut self, ui: &mut Ui, ip: IpAddr, stats: &Option) { + fn update_device(&mut self, ui: &mut Ui, ip: &str, stats: &Option) { ui.horizontal(|ui| { ui.add(Button::new("Update")) .clicked() @@ -198,8 +194,8 @@ impl Device { } } - fn check_update(&mut self, ip: IpAddr) -> anyhow::Result<()> { - let stats = status::Status::get_stats(ip).context("Failed to get stats")?; + fn check_update(&mut self, ip: &str) -> anyhow::Result<()> { + let stats = status::get_stats(ip).context("Failed to get stats")?; let current = Device::parse_to_version(&stats.version); let latest = Device::parse_to_version(&Device::get_latest_tag().unwrap_or_default()); if current < latest { @@ -245,8 +241,7 @@ impl Device { } } - fn set_update(ip: IpAddr) -> anyhow::Result<()> { - config::check_ip(ip)?; + fn set_update(ip: &str) -> anyhow::Result<()> { Client::new() .post(format!("http://{ip}/api/doupdate")) .body(String::new()) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index e242561..3a390f6 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,20 +1,20 @@ use eframe::CreationContext; use egui::{ - vec2, CentralPanel, Color32, ColorImage, Context, ImageData, TextStyle, TextureOptions, + vec2, CentralPanel, Color32, ColorImage, Context, ImageData, ScrollArea, TextStyle, + TextureOptions, }; use egui_notify::Toasts; +use parking_lot::RwLock; +use status::Stat; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use crate::config::Config; -use self::custom::Custom; use self::device::Device; -use self::screen::Screen; use self::settings::Settings; -use self::status::Status; use self::statusbar::StatusBar; -mod custom; mod device; mod screen; mod settings; @@ -25,12 +25,11 @@ pub struct App { current_tab: Tab, config: Config, toasts: Toasts, - stats: Status, device: Device, - screen: Screen, settings: Settings, statusbar: StatusBar, - custom: Custom, + pub stat: Option, + screen_texture: Arc>, } #[derive(PartialEq)] @@ -38,7 +37,6 @@ enum Tab { Screen, Status, Settings, - Custom, } impl App { @@ -56,26 +54,26 @@ impl App { let screen_texture = cc.egui_ctx.load_texture( "screen", - ImageData::Color(Arc::new(ColorImage::new([1, 10], Color32::TRANSPARENT))), + ImageData::Color(Arc::new(ColorImage::new([320, 80], Color32::TRANSPARENT))), TextureOptions::default(), ); + let screen_texture = Arc::new(RwLock::new(screen_texture)); Self { current_tab: Tab::Screen, config: Config::new(), toasts: Toasts::new().with_anchor(egui_notify::Anchor::BottomLeft), - stats: Status::new(), device: Device::new(), - screen: Screen::new(screen_texture), + screen_texture, settings: Settings::new(), statusbar: StatusBar::new(), - custom: Custom::new(), + stat: None, } } } + /// Main application loop (called every frame) impl eframe::App for App { fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) { - self.config.check_status(false); CentralPanel::default().show(ctx, |ui| { self.statusbar .show(ui, &mut self.current_tab, &mut self.config); @@ -85,23 +83,21 @@ impl eframe::App for App { self.device.show( ui, - self.config.ip, - &self.stats.stat, + &self.config.ip, + &self.stat, &mut self.settings.write_boot, ); - if self.config.last_state { - if let Some(ip) = self.config.ip { - match self.current_tab { - Tab::Status => self.stats.show(ui, ip), - Tab::Screen => self.screen.show(ui, ip), - Tab::Settings => self.settings.show(ui, ip), - Tab::Custom => self.custom.show(ui), - } + if !self.config.ip.is_empty() { + let ip = &self.config.ip; + match self.current_tab { + Tab::Status => status::show(ui, ip, &mut self.stat), + Tab::Screen => screen::show(ui, ip, self.screen_texture.clone()), + Tab::Settings => self.settings.show(ui, ip), } - } else { - ui.label("Your Awtrix Clock seems to be offline"); - ui.button("Reconnect").clicked().then(|| { - self.config.check_status(true); + .unwrap_or_else(|e| { + self.toasts + .error(e.to_string()) + .set_duration(Some(std::time::Duration::from_secs(5))); }); } }); diff --git a/src/ui/screen.rs b/src/ui/screen.rs index f80b9d6..0778eb3 100644 --- a/src/ui/screen.rs +++ b/src/ui/screen.rs @@ -1,70 +1,73 @@ -use std::{net::IpAddr, time::Duration}; +use std::sync::{mpsc, Arc}; use anyhow::Context; use egui::{ColorImage, TextureHandle, TextureOptions, Ui}; -use egui_notify::Toasts; -use reqwest::blocking::get; +use parking_lot::RwLock; +use reqwest::blocking; -pub struct Screen { - texture: TextureHandle, - size: [usize; 2], - toasts: Toasts, -} +const SIZE: [usize; 2] = [320, 80]; -impl Screen { - pub fn new(texture: TextureHandle) -> Self { - Self { - texture, - size: [320, 80], - toasts: Toasts::new().with_anchor(egui_notify::Anchor::BottomLeft), - } +pub fn show(ui: &mut Ui, ip: &str, texture: Arc>) -> anyhow::Result<()> { + if ui.button("Refresh").clicked() { + return threaded_screen(ip.to_string(), texture); } - pub fn show(&mut self, ui: &mut Ui, ip: IpAddr) { - ui.button("Refresh").clicked().then(|| { - match self.get_screen(ip) { - Ok(image) => self.texture.set(image, TextureOptions::default()), - Err(e) => { - self.toasts - .error(e.to_string()) - .set_duration(Some(Duration::from_secs(5))); - } - }; - }); + ui.image(&texture.read().clone()); + Ok(()) +} - ui.image(&self.texture); - self.toasts.show(ui.ctx()); - } +#[allow(clippy::expect_used)] +fn threaded_screen(ip: String, texture: Arc>) -> anyhow::Result<()> { + let (tx, rx) = mpsc::channel(); - fn get_screen(&mut self, ip: IpAddr) -> anyhow::Result { - let response = match get(format!("http://{ip}/api/screen")) { - Ok(response) if response.status().is_success() => response, - _ => anyhow::bail!("Failed to get screen"), + std::thread::spawn(move || { + let result = match get_screen(&ip) { + Ok(image) => { + texture.write().set(image, TextureOptions::default()); + Ok(()) + } + Err(e) => Err(e), }; - let pixels = response - .text()? - .trim_matches(|c| c == '[' || c == ']') - .split(',') - .filter_map(|s| s.parse().ok()) - .collect::>() - .into_iter() - .flat_map(|x: u32| { - [ - ((x >> 16) & 0xFF) as u8, - ((x >> 8) & 0xFF) as u8, - (x & 0xFF) as u8, - ] - }) - .collect::>(); - Ok(ColorImage::from_rgb( - self.size, - &image::imageops::resize( - &image::RgbImage::from_vec(32, 8, pixels).context("Failed to create image")?, - self.size[0] as u32, - self.size[1] as u32, - image::imageops::FilterType::Nearest, - ), - )) + tx.send(result).expect("Failed to send result"); + std::thread::sleep(std::time::Duration::from_secs(1)); + }); + + // Wait for the result from the thread + match rx.recv() { + Ok(result) => result, + Err(_) => Err(anyhow::anyhow!("Failed to receive result from thread")), } } + +fn get_screen(ip: &str) -> anyhow::Result { + let response = match blocking::get(format!("http://{ip}/api/screen")) { + Ok(response) if response.status().is_success() => response, + _ => anyhow::bail!("Failed to get screen"), + }; + let pixels = response + .text()? + .trim_matches(|c| c == '[' || c == ']') + .split(',') + .filter_map(|s| s.parse().ok()) + .collect::>() + .into_iter() + .flat_map(|x: u32| { + [ + ((x >> 16) & 0xFF) as u8, + ((x >> 8) & 0xFF) as u8, + (x & 0xFF) as u8, + ] + }) + .collect::>(); + + Ok(ColorImage::from_rgb( + SIZE, + &image::imageops::resize( + &image::RgbImage::from_vec(32, 8, pixels).context("Failed to create image")?, + SIZE[0] as u32, + SIZE[1] as u32, + image::imageops::FilterType::Nearest, + ), + )) +} diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 02d3af3..bf40475 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -1,4 +1,4 @@ -use std::{net::IpAddr, time::Duration}; +use std::time::Duration; use anyhow::Context; use egui::{Align, Button, Layout, ScrollArea, TextEdit, TextStyle, Ui}; @@ -9,7 +9,6 @@ use reqwest::blocking::{get, Client}; pub struct Settings { language: String, code: String, - toasts: Toasts, pub write_boot: (bool, bool), } @@ -18,58 +17,49 @@ impl Settings { Self { language: "json".to_string(), code: String::new(), - toasts: Toasts::new().with_anchor(egui_notify::Anchor::BottomLeft), write_boot: (false, false), } } - pub fn show(&mut self, ui: &mut Ui, ip: IpAddr) { + // TODO: Remove this lint suppression + #[allow(clippy::unnecessary_wraps)] + pub fn show(&mut self, ui: &mut Ui, ip: &str) -> anyhow::Result<()> { ui.horizontal(|ui| { - ui.add(Button::new("Get Settings")).clicked().then(|| { - self.code = match Self::get_settings(ip) { - Ok(settings) => settings, + if ui.add(Button::new("Get Settings")).clicked() { + match Self::get_settings(ip) { + Ok(settings) => { + self.code = settings; + return Ok(()); + } Err(e) => { - self.toasts - .error(format!("Failed to get settings: {e}")) - .set_duration(Some(Duration::from_secs(5))); - String::new() + self.code = String::new(); + anyhow::bail!(e) } }; - }); - ui.add(Button::new("Write Settings")) - .clicked() - .then(|| match self.set_settings(ip) { + } + if ui.add(Button::new("Write Settings")).clicked() { + match self.set_settings(ip) { Ok(()) => { self.write_boot = (true, false); - self.toasts - .info("Settings written") - .set_duration(Some(Duration::from_secs(5))); + return Ok(()); } - Err(e) => { - self.toasts - .error(format!("Failed to write settings: {e}")) - .set_duration(Some(Duration::from_secs(5))); - } - }); + Err(e) => anyhow::bail!(e), + } + } if self.write_boot.0 && !self.write_boot.1 { ui.label("Reboot required!"); } ui.with_layout(Layout::right_to_left(Align::Center), |ui| { ui.spacing(); - ui.add(Button::new(" i ").rounding(40.0)) - .clicked() - .then(|| { - if open::that( - "https://blueforcer.github.io/awtrix3/#/api?id=change-settings", - ) + if ui.add(Button::new(" i ").rounding(40.0)).clicked() + && open::that("https://blueforcer.github.io/awtrix3/#/api?id=change-settings") .is_err() - { - self.toasts - .error("Failed to open browser") - .set_duration(Some(Duration::from_secs(5))); - } - }); + { + anyhow::bail!("Failed to open browser") + } + Ok(()) }); + Ok(()) }); let theme = syntax_highlighting::CodeTheme::from_style(ui.style()); let mut layouter = |ui: &Ui, string: &str, wrap_width: f32| { @@ -89,10 +79,10 @@ impl Settings { .layouter(&mut layouter), ); }); - self.toasts.show(ui.ctx()); + Ok(()) } - fn get_settings(ip: IpAddr) -> anyhow::Result { + fn get_settings(ip: &str) -> anyhow::Result { let response = get(format!("http://{ip}/api/settings")) .map_err(|_| anyhow::anyhow!("Failed to get settings"))?; @@ -103,7 +93,7 @@ impl Settings { .replace('}', "\n}")) } - pub fn set_settings(&mut self, ip: IpAddr) -> anyhow::Result<()> { + pub fn set_settings(&mut self, ip: &str) -> anyhow::Result<()> { Client::new() .post(format!("http://{ip}/api/settings")) .body(self.code.clone()) diff --git a/src/ui/status.rs b/src/ui/status.rs index 6479f51..eda3cd4 100644 --- a/src/ui/status.rs +++ b/src/ui/status.rs @@ -1,62 +1,43 @@ -use std::{fmt::Display, net::IpAddr, time::Duration}; +use std::fmt::Display; use egui::{Button, Ui}; -use egui_notify::Toasts; use reqwest::blocking::get; use serde::{Deserialize, Serialize}; use serde_json::from_str; -use crate::config; - -pub struct Status { - pub stat: Option, - toasts: Toasts, -} - -impl Status { - pub fn new() -> Self { - Self { - stat: None, - toasts: Toasts::new().with_anchor(egui_notify::Anchor::BottomLeft), - } - } - - pub fn show(&mut self, ui: &mut Ui, ip: IpAddr) { - if ui.add(Button::new("Refresh")).clicked() { - self.stat = if let Ok(stat) = Self::get_stats(ip) { - Some(stat) - } else { - self.toasts - .error("Failed to get stats") - .set_duration(Some(Duration::from_secs(5))); - None +pub fn show(ui: &mut Ui, ip: &str, stat: &mut Option) -> anyhow::Result<()> { + if ui.add(Button::new("Refresh")).clicked() { + match get_stats(ip) { + Ok(s) => { + *stat = Some(s); + return Ok(()); } + Err(e) => anyhow::bail!(e), } - ui.add(egui::Label::new(Self::get_string(&self.stat).to_string())); - self.toasts.show(ui.ctx()); } + ui.add(egui::Label::new(get_string(stat).to_string())); + Ok(()) +} - pub fn get_stats(ip: IpAddr) -> anyhow::Result { - config::check_ip(ip)?; - let response = match get(format!("http://{ip}/api/stats")) { - Ok(response) if response.status().is_success() => response, - _ => anyhow::bail!("Failed to get stats"), - }; - Ok(from_str(&response.text()?)?) - } +pub fn get_stats(ip: &str) -> anyhow::Result { + let response = match get(format!("http://{ip}/api/stats")) { + Ok(response) if response.status().is_success() => response, + _ => anyhow::bail!("Failed to get stats"), + }; + Ok(from_str(&response.text()?)?) +} - fn get_string(stat: &Option) -> String { - if let Some(stat) = stat { - format!( +fn get_string(stat: &Option) -> String { + if let Some(stat) = stat { + format!( "Battery: {}%\nBattery Raw: {}\nData Type: {}\nLux: {}\nLDR Raw: {}\nRAM: {}%\nBrightness: {}\nTemperature: {}°C\nHumidity: {}%\nUptime: {}s\nWiFi Signal: {}%\nMessages: {}\nVersion: {}\nIndicator 1: {}\nIndicator 2: {}\nIndicator 3: {}\nApp: {}\nUID: {}\nMatrix: {}\nIP Address: {}", stat.bat, stat.bat_raw, stat.data_type, stat.lux, stat.ldr_raw, stat.ram, stat.bri, stat.temp, stat.hum, stat.uptime, stat.wifi_signal, stat.messages, stat.version, stat.indicator1, stat.indicator2, stat.indicator3, stat.app, stat.uid, stat.matrix, stat.ip_address ) - } else { - "Battery: N/A\nBattery Raw: N/A\nData Type: N/A\nLux: N/A\nLDR Raw: N/A\nRAM: N/A\nBrightness: N/A\nTemperature: N/A\nHumidity: N/A\nUptime: N/A\nWiFi Signal: N/A\nMessages: N/A\nVersion: N/A\nIndicator 1: N/A\nIndicator 2: N/A\nIndicator 3: N/A\nApp: N/A\nUID: N/A\nMatrix: N/A\nIP Address: N/A".to_string() - } + } else { + "Battery: N/A\nBattery Raw: N/A\nData Type: N/A\nLux: N/A\nLDR Raw: N/A\nRAM: N/A\nBrightness: N/A\nTemperature: N/A\nHumidity: N/A\nUptime: N/A\nWiFi Signal: N/A\nMessages: N/A\nVersion: N/A\nIndicator 1: N/A\nIndicator 2: N/A\nIndicator 3: N/A\nApp: N/A\nUID: N/A\nMatrix: N/A\nIP Address: N/A".to_string() } } diff --git a/src/ui/statusbar.rs b/src/ui/statusbar.rs index b55d642..80bfce5 100644 --- a/src/ui/statusbar.rs +++ b/src/ui/statusbar.rs @@ -1,5 +1,3 @@ -use std::time::Duration; - use crate::config::Config; use egui::{ include_image, special_emojis::GITHUB, vec2, Align, Align2, Button, Frame, Image, Layout, @@ -24,28 +22,21 @@ impl StatusBar { pub fn show(&mut self, ui: &mut Ui, tab: &mut Tab, config: &mut Config) { ui.horizontal(|ui| { - if config.last_state { + ui.add_enabled_ui(!config.ip.is_empty(), |ui| { ui.selectable_value(tab, Tab::Screen, "Screen"); ui.selectable_value(tab, Tab::Status, "Status"); ui.selectable_value(tab, Tab::Settings, "Settings"); - ui.selectable_value(tab, Tab::Custom, "Custom"); - } + }); ui.with_layout(Layout::right_to_left(Align::Center), |ui| { ui.add(Button::new(" ? ").rounding(40.0)) .clicked() .then(|| self.show_about = true); - ui.add(Button::new("Set")).clicked().then(|| { - if config.set_ip().is_err() { - self.toasts - .error("Failed to set IP") - .set_duration(Some(Duration::from_secs(5))); - } else { - config.check_status(true); - } + ui.add(Button::new("Save")).clicked().then(|| { + config.write().unwrap(); }); ui.add( - TextEdit::singleline(&mut config.ip_str) + TextEdit::singleline(&mut config.ip) .hint_text("IP") .desired_width(150.0), ); From 5089a606f15c4052d4fddf4ac966139e5368e76f Mon Sep 17 00:00:00 2001 From: Nicolas Date: Fri, 7 Jun 2024 16:45:58 +0200 Subject: [PATCH 2/8] Update Cargo.toml --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b8e93a0..2415180 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,5 +38,5 @@ egui = "0.27.2" egui-notify = "0.14.0" egui_extras = { version = "0.27.2", features = ["syntect", "image"] } image = "0.25.1" -open = "5.1.3" +open = "5.1.4" parking_lot = "0.12.3" From 38f7b2a374bf15e288b8f88356f3bf5d8315f136 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Fri, 7 Jun 2024 17:00:23 +0200 Subject: [PATCH 3/8] fixes --- src/ui/device.rs | 2 ++ src/ui/mod.rs | 17 ++++++------ src/ui/settings.rs | 3 --- src/ui/statusbar.rs | 63 ++++++++++++++++++++++++--------------------- 4 files changed, 44 insertions(+), 41 deletions(-) diff --git a/src/ui/device.rs b/src/ui/device.rs index 8021323..60f78cd 100644 --- a/src/ui/device.rs +++ b/src/ui/device.rs @@ -58,6 +58,7 @@ impl Device { self.toasts.show(ui.ctx()); } + /* fn power(&mut self, ui: &mut Ui, ip: &str) { fn handle_power_result(result: &anyhow::Result<()>, toasts: &mut Toasts, power: &str) { match result { @@ -92,6 +93,7 @@ impl Device { .then_some(()) .context("Failed to set power") } + */ fn sleep(&mut self, ui: &mut Ui, ip: &str) { ui.horizontal(|ui| { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 3a390f6..298225a 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,12 +1,10 @@ use eframe::CreationContext; use egui::{ - vec2, CentralPanel, Color32, ColorImage, Context, ImageData, ScrollArea, TextStyle, - TextureOptions, + vec2, CentralPanel, Color32, ColorImage, Context, ImageData, TextStyle, TextureOptions, }; use egui_notify::Toasts; use parking_lot::RwLock; use status::Stat; -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use crate::config::Config; @@ -75,8 +73,13 @@ impl App { impl eframe::App for App { fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) { CentralPanel::default().show(ctx, |ui| { - self.statusbar - .show(ui, &mut self.current_tab, &mut self.config); + match self + .statusbar + .show(ui, &mut self.current_tab, &mut self.config) + { + Ok(()) => self.toasts.success("Config saved!"), + Err(e) => self.toasts.error(e.to_string()), + }; ui.vertical_centered(|ui| { ui.separator(); }); @@ -95,9 +98,7 @@ impl eframe::App for App { Tab::Settings => self.settings.show(ui, ip), } .unwrap_or_else(|e| { - self.toasts - .error(e.to_string()) - .set_duration(Some(std::time::Duration::from_secs(5))); + self.toasts.error(e.to_string()); }); } }); diff --git a/src/ui/settings.rs b/src/ui/settings.rs index bf40475..5764270 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -1,9 +1,6 @@ -use std::time::Duration; - use anyhow::Context; use egui::{Align, Button, Layout, ScrollArea, TextEdit, TextStyle, Ui}; use egui_extras::syntax_highlighting; -use egui_notify::Toasts; use reqwest::blocking::{get, Client}; pub struct Settings { diff --git a/src/ui/statusbar.rs b/src/ui/statusbar.rs index 80bfce5..a783aa5 100644 --- a/src/ui/statusbar.rs +++ b/src/ui/statusbar.rs @@ -3,48 +3,51 @@ use egui::{ include_image, special_emojis::GITHUB, vec2, Align, Align2, Button, Frame, Image, Layout, TextEdit, Ui, Window, }; -use egui_notify::Toasts; use super::Tab; pub struct StatusBar { show_about: bool, - toasts: Toasts, } impl StatusBar { pub fn new() -> Self { - Self { - show_about: false, - toasts: Toasts::new().with_anchor(egui_notify::Anchor::BottomLeft), - } + Self { show_about: false } } - pub fn show(&mut self, ui: &mut Ui, tab: &mut Tab, config: &mut Config) { - ui.horizontal(|ui| { - ui.add_enabled_ui(!config.ip.is_empty(), |ui| { - ui.selectable_value(tab, Tab::Screen, "Screen"); - ui.selectable_value(tab, Tab::Status, "Status"); - ui.selectable_value(tab, Tab::Settings, "Settings"); - }); - - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - ui.add(Button::new(" ? ").rounding(40.0)) - .clicked() - .then(|| self.show_about = true); - ui.add(Button::new("Save")).clicked().then(|| { - config.write().unwrap(); - }); - ui.add( - TextEdit::singleline(&mut config.ip) - .hint_text("IP") - .desired_width(150.0), - ); - ui.label("IP:"); - }); - }); + pub fn show(&mut self, ui: &mut Ui, tab: &mut Tab, config: &mut Config) -> anyhow::Result<()> { self.about_window(ui); - self.toasts.show(ui.ctx()); + return ui + .horizontal(|ui| { + ui.add_enabled_ui(!config.ip.is_empty(), |ui| { + ui.selectable_value(tab, Tab::Screen, "Screen"); + ui.selectable_value(tab, Tab::Status, "Status"); + ui.selectable_value(tab, Tab::Settings, "Settings"); + }); + + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + ui.add(Button::new(" ? ").rounding(40.0)) + .clicked() + .then(|| self.show_about = true); + let ret = ui + .add(Button::new("Save")) + .clicked() + .then(|| match config.write() { + Ok(()) => anyhow::Ok(()), + Err(e) => anyhow::bail!(e), + }) + .unwrap_or(Ok(())); + ui.add( + TextEdit::singleline(&mut config.ip) + .hint_text("IP") + .desired_width(150.0), + ); + ui.label("IP:"); + ret + }) + .inner + }) + .inner; } fn about_window(&mut self, ui: &mut Ui) { From e58660ae28ac47f20e856fca2e0f4fea505c027e Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 8 Jun 2024 00:05:07 +0200 Subject: [PATCH 4/8] anyhow::result --- src/ui/device.rs | 144 +++++++++++++++++---------------------------- src/ui/mod.rs | 11 ++-- src/ui/settings.rs | 10 +--- 3 files changed, 60 insertions(+), 105 deletions(-) diff --git a/src/ui/device.rs b/src/ui/device.rs index 60f78cd..bfa4945 100644 --- a/src/ui/device.rs +++ b/src/ui/device.rs @@ -1,8 +1,5 @@ -use std::time::Duration; - use anyhow::Context; use egui::{Button, DragValue, Label, ScrollArea, SidePanel, Ui}; -use egui_notify::Toasts; use reqwest::blocking::Client; use semver::Version; use serde_json::{from_str, Value}; @@ -10,7 +7,6 @@ use serde_json::{from_str, Value}; use super::status::{self, Stat}; pub struct Device { - toasts: Toasts, time: i32, update_available: bool, } @@ -18,44 +14,42 @@ pub struct Device { impl Device { pub fn new() -> Self { Self { - toasts: Toasts::new().with_anchor(egui_notify::Anchor::BottomLeft), time: 0, update_available: false, } } - pub fn show( - &mut self, - ui: &mut Ui, - ip: &str, - stats: &Option, - write_boot: &mut (bool, bool), - ) { + pub fn show(&mut self, ui: &mut Ui, ip: &str, stats: &Option) -> anyhow::Result<()> { SidePanel::right("panel") .show_separator_line(true) .show_inside(ui, |ui| { - ScrollArea::new([false, true]).show(ui, |ui| { - ui.horizontal(|ui| { - ui.heading("Awtrix Options"); - }); - ui.separator(); - if ip.is_empty() { - ui.label("No IP set"); - } else { - ui.vertical_centered(|ui| { - ui.horizontal(|ui| { - // TODO: FIX THIS - //self.power(ui, ip); - self.reboot(ui, ip, write_boot); - ui.separator(); - self.sleep(ui, ip); - }); + ScrollArea::new([false, true]) + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.heading("Awtrix Options"); }); - self.update_device(ui, ip, stats); - } - }); - }); - self.toasts.show(ui.ctx()); + ui.separator(); + if ip.is_empty() { + ui.label("No IP set"); + Ok(()) + } else { + ui.vertical_centered(|ui| { + ui.horizontal(|ui| { + // TODO: FIX THIS + //self.power(ui, ip); + // TODO: FIX THIS + let _ = Self::reboot(ui, ip); + ui.separator(); + self.sleep(ui, ip) + }); + }); + self.update_device(ui, ip, stats) + } + }) + .inner + }) + .inner + //Ok(()) } /* @@ -95,7 +89,7 @@ impl Device { } */ - fn sleep(&mut self, ui: &mut Ui, ip: &str) { + fn sleep(&mut self, ui: &mut Ui, ip: &str) -> anyhow::Result<()> { ui.horizontal(|ui| { ui.add( DragValue::new(&mut self.time) @@ -103,19 +97,10 @@ impl Device { .clamp_range(0..=3600) .suffix("s"), ); - ui.button("Sleep") - .clicked() - .then(|| match self.set_sleep(ip) { - Ok(()) => self - .toasts - .info(format!("Sleep set to: {}s", self.time)) - .set_duration(Some(Duration::from_secs(5))), - Err(_) => self - .toasts - .error("Failed to set sleep") - .set_duration(Some(Duration::from_secs(5))), - }); - }); + ui.button("Sleep").clicked().then(|| self.set_sleep(ip)) + }) + .inner + .unwrap_or(Ok(())) } fn set_sleep(&self, ip: &str) -> anyhow::Result<()> { @@ -130,22 +115,11 @@ impl Device { .context("Failed to set sleep") } - fn reboot(&mut self, ui: &mut Ui, ip: &str, write_boot: &mut (bool, bool)) { + fn reboot(ui: &mut Ui, ip: &str) -> anyhow::Result<()> { ui.button("Reboot") .clicked() - .then(|| match Self::set_reboot(ip) { - Ok(()) => { - write_boot.0 = false; - write_boot.1 = false; - self.toasts - .info("Rebooting device") - .set_duration(Some(Duration::from_secs(5))) - } - Err(_) => self - .toasts - .error("Failed to reboot") - .set_duration(Some(Duration::from_secs(5))), - }); + .then(|| Self::set_reboot(ip)) + .unwrap_or(Ok(())) } fn set_reboot(ip: &str) -> anyhow::Result<()> { @@ -159,41 +133,31 @@ impl Device { .context("Failed to reboot") } - fn update_device(&mut self, ui: &mut Ui, ip: &str, stats: &Option) { - ui.horizontal(|ui| { - ui.add(Button::new("Update")) - .clicked() - .then(|| match self.check_update(ip) { - Ok(()) => self - .toasts - .info("Update available") - .set_duration(Some(Duration::from_secs(5))), - Err(e) => self - .toasts - .info(e.to_string()) - .set_duration(Some(Duration::from_secs(5))), - }); - if let Some(stats) = stats { - ui.label(format!("Version: {}", stats.version)); - } - }); + fn update_device(&mut self, ui: &mut Ui, ip: &str, stats: &Option) -> anyhow::Result<()> { + let mut ret = ui + .horizontal(|ui| { + let ret = ui + .add(Button::new("Update")) + .clicked() + .then(|| self.check_update(ip)); + if let Some(stats) = stats { + ui.label(format!("Version: {}", stats.version)); + } + ret + }) + .inner + .unwrap_or(Ok(())); if self.update_available { ui.add(Label::new("An Update is available!")); - ui.add(Button::new("Update now")) + ret = ui + .add(Button::new("Update now")) .on_hover_text("Update device") .clicked() - .then(|| match Self::set_update(ip) { - Ok(()) => self - .toasts - .info("Updating device") - .set_duration(Some(Duration::from_secs(5))), - Err(_) => self - .toasts - .error("Failed to update") - .set_duration(Some(Duration::from_secs(5))), - }); + .then(|| Self::set_update(ip)) + .unwrap_or(Ok(())); } + ret } fn check_update(&mut self, ip: &str) -> anyhow::Result<()> { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 298225a..69020b9 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -84,12 +84,11 @@ impl eframe::App for App { ui.separator(); }); - self.device.show( - ui, - &self.config.ip, - &self.stat, - &mut self.settings.write_boot, - ); + self.device + .show(ui, &self.config.ip, &self.stat) + .unwrap_or_else(|e| { + self.toasts.error(e.to_string()); + }); if !self.config.ip.is_empty() { let ip = &self.config.ip; match self.current_tab { diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 5764270..dcdfe6f 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -6,7 +6,6 @@ use reqwest::blocking::{get, Client}; pub struct Settings { language: String, code: String, - pub write_boot: (bool, bool), } impl Settings { @@ -14,7 +13,6 @@ impl Settings { Self { language: "json".to_string(), code: String::new(), - write_boot: (false, false), } } @@ -36,16 +34,10 @@ impl Settings { } if ui.add(Button::new("Write Settings")).clicked() { match self.set_settings(ip) { - Ok(()) => { - self.write_boot = (true, false); - return Ok(()); - } + Ok(()) => return Ok(()), Err(e) => anyhow::bail!(e), } } - if self.write_boot.0 && !self.write_boot.1 { - ui.label("Reboot required!"); - } ui.with_layout(Layout::right_to_left(Align::Center), |ui| { ui.spacing(); if ui.add(Button::new(" i ").rounding(40.0)).clicked() From 2d4ad4babeeacf9dd915c4e3682650b6992bfba8 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 9 Jun 2024 15:35:42 +0200 Subject: [PATCH 5/8] update --- src/ui/mod.rs | 39 ++++++++++++++++++++++++++------------- src/ui/screen.rs | 8 ++++++-- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 69020b9..c012d1b 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,3 +1,4 @@ +use anyhow::Ok; use eframe::CreationContext; use egui::{ vec2, CentralPanel, Color32, ColorImage, Context, ImageData, TextStyle, TextureOptions, @@ -20,7 +21,7 @@ mod status; mod statusbar; pub struct App { - current_tab: Tab, + current_tab: Arc>, config: Config, toasts: Toasts, device: Device, @@ -37,6 +38,16 @@ enum Tab { Settings, } +impl Tab { + fn as_str(&self) -> &str { + match self { + Tab::Screen => "Screen", + Tab::Status => "Status", + Tab::Settings => "Settings", + } + } +} + impl App { pub fn new(cc: &CreationContext) -> Self { egui_extras::install_image_loaders(&cc.egui_ctx); @@ -56,8 +67,9 @@ impl App { TextureOptions::default(), ); let screen_texture = Arc::new(RwLock::new(screen_texture)); + let current_tab = Arc::new(RwLock::new(Tab::Status)); Self { - current_tab: Tab::Screen, + current_tab, config: Config::new(), toasts: Toasts::new().with_anchor(egui_notify::Anchor::BottomLeft), device: Device::new(), @@ -72,14 +84,14 @@ impl App { /// Main application loop (called every frame) impl eframe::App for App { fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) { + let mut current_tab = self.current_tab.write(); CentralPanel::default().show(ctx, |ui| { - match self - .statusbar - .show(ui, &mut self.current_tab, &mut self.config) - { - Ok(()) => self.toasts.success("Config saved!"), - Err(e) => self.toasts.error(e.to_string()), - }; + self.statusbar + .show(ui, &mut current_tab, &mut self.config) + .unwrap_or_else(|e| { + self.toasts.error(e.to_string()); + }); + ui.vertical_centered(|ui| { ui.separator(); }); @@ -91,10 +103,11 @@ impl eframe::App for App { }); if !self.config.ip.is_empty() { let ip = &self.config.ip; - match self.current_tab { - Tab::Status => status::show(ui, ip, &mut self.stat), - Tab::Screen => screen::show(ui, ip, self.screen_texture.clone()), - Tab::Settings => self.settings.show(ui, ip), + match current_tab.as_str() { + "Status" => status::show(ui, ip, &mut self.stat), + "Screen" => screen::show(ui, ip, self.screen_texture.clone()), + "Settings" => self.settings.show(ui, ip), + _ => Ok(()), } .unwrap_or_else(|e| { self.toasts.error(e.to_string()); diff --git a/src/ui/screen.rs b/src/ui/screen.rs index 0778eb3..0c893a7 100644 --- a/src/ui/screen.rs +++ b/src/ui/screen.rs @@ -3,7 +3,6 @@ use std::sync::{mpsc, Arc}; use anyhow::Context; use egui::{ColorImage, TextureHandle, TextureOptions, Ui}; use parking_lot::RwLock; -use reqwest::blocking; const SIZE: [usize; 2] = [320, 80]; @@ -41,7 +40,12 @@ fn threaded_screen(ip: String, texture: Arc>) -> anyhow::R } fn get_screen(ip: &str) -> anyhow::Result { - let response = match blocking::get(format!("http://{ip}/api/screen")) { + let client = reqwest::blocking::Client::new(); + let response = match client + .get(format!("http://{ip}/api/screen")) + .timeout(std::time::Duration::from_secs(5)) + .send() + { Ok(response) if response.status().is_success() => response, _ => anyhow::bail!("Failed to get screen"), }; From 77861b031654c88dea484e024ffbc13242b26ace Mon Sep 17 00:00:00 2001 From: Nicolas Date: Wed, 3 Jul 2024 19:29:03 +0200 Subject: [PATCH 6/8] Bump egui --- Cargo.toml | 14 +++++++------- src/main.rs | 3 +-- src/ui/device.rs | 50 ++++++++++++++++++------------------------------ 3 files changed, 27 insertions(+), 40 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2415180..0dddd76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,16 +27,16 @@ short_description = "A GUI for the awtrix clock." # Error handling anyhow = "1.0.86" # Networking -reqwest = { version = "0.12.4", features = ["blocking"] } +reqwest = { version = "0.12.5", features = ["blocking"] } # Parsing serde = { version = "1.0.203", features = ["derive"] } -serde_json = "1.0.117" +serde_json = "1.0.120" semver = "1.0.23" # GUI -eframe = { version = "0.27.2", features = ["wgpu"] } -egui = "0.27.2" -egui-notify = "0.14.0" -egui_extras = { version = "0.27.2", features = ["syntect", "image"] } +eframe = "0.28.0" +egui = "0.28.0" +egui-notify = { git = "https://github.com/bircni/egui-notify", branch = "main" } +egui_extras = { version = "0.28.0", features = ["syntect", "image"] } image = "0.25.1" -open = "5.1.4" +open = "5.2.0" parking_lot = "0.12.3" diff --git a/src/main.rs b/src/main.rs index 9cbb2cd..5d0f840 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,12 +27,11 @@ fn main() -> anyhow::Result<()> { "Awtrix", eframe::NativeOptions { viewport, - depth_buffer: 32, follow_system_theme: true, centered: true, ..Default::default() }, - Box::new(|cc| Box::new(ui::App::new(cc))), + Box::new(|cc| Ok(Box::new(ui::App::new(cc)))), ) .map_err(|e| anyhow::anyhow!(e.to_string())) .context("Failed to run native") diff --git a/src/ui/device.rs b/src/ui/device.rs index bfa4945..cc2e097 100644 --- a/src/ui/device.rs +++ b/src/ui/device.rs @@ -36,14 +36,14 @@ impl Device { ui.vertical_centered(|ui| { ui.horizontal(|ui| { // TODO: FIX THIS - //self.power(ui, ip); + Self::power(ui, ip); // TODO: FIX THIS - let _ = Self::reboot(ui, ip); + Self::reboot(ui, ip); ui.separator(); self.sleep(ui, ip) }); }); - self.update_device(ui, ip, stats) + self.update_device(ui, ip, stats, self.update_available) } }) .inner @@ -52,26 +52,12 @@ impl Device { //Ok(()) } - /* - fn power(&mut self, ui: &mut Ui, ip: &str) { - fn handle_power_result(result: &anyhow::Result<()>, toasts: &mut Toasts, power: &str) { - match result { - Ok(()) => toasts - .info(format!("Power {power}")) - .set_duration(Some(Duration::from_secs(5))), - Err(_) => toasts - .error("Failed to set power") - .set_duration(Some(Duration::from_secs(5))), - }; - } - + fn power(ui: &mut Ui, ip: &str) { ui.horizontal(|ui| { - ui.button("On").clicked().then(|| { - handle_power_result(&Self::set_power(ip, true), &mut self.toasts, "On"); - }); - ui.button("Off").clicked().then(|| { - handle_power_result(&Self::set_power(ip, true), &mut self.toasts, "Off"); - }); + ui.button("On").clicked().then(|| Self::set_power(ip, true)); + ui.button("Off") + .clicked() + .then(|| Self::set_power(ip, false)); }); } @@ -87,14 +73,13 @@ impl Device { .then_some(()) .context("Failed to set power") } - */ fn sleep(&mut self, ui: &mut Ui, ip: &str) -> anyhow::Result<()> { ui.horizontal(|ui| { ui.add( DragValue::new(&mut self.time) .speed(1.0) - .clamp_range(0..=3600) + .range(0..=3600) .suffix("s"), ); ui.button("Sleep").clicked().then(|| self.set_sleep(ip)) @@ -115,11 +100,8 @@ impl Device { .context("Failed to set sleep") } - fn reboot(ui: &mut Ui, ip: &str) -> anyhow::Result<()> { - ui.button("Reboot") - .clicked() - .then(|| Self::set_reboot(ip)) - .unwrap_or(Ok(())) + fn reboot(ui: &mut Ui, ip: &str) { + ui.button("Reboot").clicked().then(|| Self::set_reboot(ip)); } fn set_reboot(ip: &str) -> anyhow::Result<()> { @@ -133,11 +115,17 @@ impl Device { .context("Failed to reboot") } - fn update_device(&mut self, ui: &mut Ui, ip: &str, stats: &Option) -> anyhow::Result<()> { + fn update_device( + &mut self, + ui: &mut Ui, + ip: &str, + stats: &Option, + enabled: bool, + ) -> anyhow::Result<()> { let mut ret = ui .horizontal(|ui| { let ret = ui - .add(Button::new("Update")) + .add_enabled(enabled, Button::new("Update")) .clicked() .then(|| self.check_update(ip)); if let Some(stats) = stats { From 16984f2ed190cdc9db5fc3308d4208c335633e2a Mon Sep 17 00:00:00 2001 From: Nicolas Date: Tue, 9 Jul 2024 17:20:41 +0200 Subject: [PATCH 7/8] Bump versions --- Cargo.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0dddd76..15cc890 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,14 +29,14 @@ anyhow = "1.0.86" # Networking reqwest = { version = "0.12.5", features = ["blocking"] } # Parsing -serde = { version = "1.0.203", features = ["derive"] } +serde = { version = "1.0.204", features = ["derive"] } serde_json = "1.0.120" semver = "1.0.23" # GUI -eframe = "0.28.0" -egui = "0.28.0" -egui-notify = { git = "https://github.com/bircni/egui-notify", branch = "main" } -egui_extras = { version = "0.28.0", features = ["syntect", "image"] } +eframe = "0.28.1" +egui = "0.28.1" +egui-notify = "0.15.0" +egui_extras = { version = "0.28.1", features = ["syntect", "image"] } image = "0.25.1" -open = "5.2.0" +open = "5.3.0" parking_lot = "0.12.3" From 67e79832214f38bcab6fcbb68f920bbb6f1757b4 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Tue, 16 Jul 2024 17:04:00 +0200 Subject: [PATCH 8/8] fix lints --- src/config.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/config.rs b/src/config.rs index 2fe52ae..f16e018 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,15 +1,13 @@ use anyhow::Context; use core::str; use serde::{Deserialize, Serialize}; -use std::{env, fs, io::Write, time::Instant}; +use std::{env, fs, io::Write}; const ENV: &str = ".awtrix.env"; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Config { pub ip: String, - #[serde(skip, default = "Instant::now")] - pub last_ping: Instant, // true = On, false = Off pub last_state: bool, } @@ -20,7 +18,6 @@ impl Config { Ok(config) => config, Err(_) => Self { ip: String::new(), - last_ping: Instant::now(), last_state: false, }, }