-
-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/widgets settings #79
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -60,6 +60,7 @@ pub struct Snapdash { | |
| pub status: String, | ||
|
|
||
| pub entity_windows: HashMap<String, window::Id>, | ||
| pub entity_windows_opening: HashSet<String>, | ||
| pub boot_open_done: bool, | ||
|
|
||
| pub settings_sensors: Vec<SettingsSensor>, | ||
|
|
@@ -90,6 +91,7 @@ pub enum Message { | |
| OpenEntity(String), | ||
| OpenReleaseNotes, | ||
| OpenUrl(String), | ||
| OpenWidgetSettings(String), | ||
| CloseWindow(window::Id), | ||
| QuitApp, | ||
| WindowClosed(window::Id), | ||
|
|
@@ -149,6 +151,12 @@ pub enum Message { | |
|
|
||
| WidgetSizeChanged(WidgetSize), | ||
| WidgetPriorityChanged(String, Priority), | ||
| WidgetNameChanged(String, String), | ||
|
|
||
| WidgetVisibilityToggled(String, bool), | ||
| WidgetVisibilityTriggerChanged(String, String), | ||
| WidgetVisibilityConditionChanged(String, crate::widget_visibility::ConditionKind), | ||
| WidgetVisibilityValueChanged(String, String), | ||
|
|
||
| InstallUpdate, | ||
| UpdateInstelled(Result<std::path::PathBuf, String>), | ||
|
|
@@ -199,6 +207,7 @@ impl Snapdash { | |
| theme_options: vec![ThemeKind::MacLight, ThemeKind::MacDark], | ||
| windows: HashMap::new(), | ||
| entity_windows: HashMap::new(), | ||
| entity_windows_opening: HashSet::new(), | ||
| boot_open_done: false, | ||
| settings_sensors: Vec::new(), | ||
| selected_widgets: HashSet::new(), | ||
|
|
@@ -411,11 +420,13 @@ impl Snapdash { | |
| } | ||
| HaEvent::InitialState(states) => { | ||
| self.apply_initial_states(states); | ||
| Task::none() | ||
|
|
||
| // we can evaluate any rules whose triggers we previously didn't know. | ||
| self.update_widget_visibility() | ||
| } | ||
| HaEvent::StateChanged { new_state } => { | ||
| self.apply_entity_state(new_state); | ||
| Task::none() | ||
| self.update_widget_visibility() | ||
| } | ||
| HaEvent::AuthFailed(error) => { | ||
| self.ha.connected = false; | ||
|
|
@@ -458,6 +469,74 @@ impl Snapdash { | |
|
|
||
| fn is_entity_window_open(&self, entity_id: &str) -> bool { | ||
| self.entity_windows.contains_key(entity_id) | ||
| || self.entity_windows_opening.contains(entity_id) | ||
| } | ||
|
|
||
| /// Resolved title for a widget: custom override (if any) → HA | ||
| /// friendly_name → bare entity_id without the domain prefix. | ||
| pub fn display_name(&self, entity_id: &str) -> String { | ||
| if let Some(name) = self | ||
| .config | ||
| .widget_names | ||
| .get(entity_id) | ||
| .filter(|s| !s.is_empty()) | ||
| { | ||
| return name.clone(); | ||
| } | ||
|
|
||
| if let Some(name) = self | ||
| .ha | ||
| .entities | ||
| .get(entity_id) | ||
| .and_then(|s| s.attributes.get("friendly_name").and_then(|v| v.as_str())) | ||
| { | ||
| return name.to_owned(); | ||
| } | ||
|
|
||
| entity_id.split('.').nth(1).unwrap_or(entity_id).to_owned() | ||
| } | ||
|
|
||
| /// `true` when a widget should currently be shown. No rule → always | ||
| /// visible. Rule → delegate to its evaluator against current HA | ||
| /// entities. | ||
| pub fn is_widget_visible(&self, entity_id: &str) -> bool { | ||
| match self.config.widget_visibility.get(entity_id) { | ||
| None => true, | ||
| Some(rule) => rule.evaluate(&self.ha.entities), | ||
| } | ||
| } | ||
|
|
||
| /// Walk every rule-gated widget and emit OpenEntity / CloseWindow | ||
| /// tasks for the deltas (newly-visible-but-not-open, or | ||
| /// currently-open-but-now-hidden). Widgets WITHOUT a rule are not | ||
| /// touched — they're under user control, otherwise closing one | ||
| /// manually would have it bounce right back open. | ||
| fn update_widget_visibility(&mut self) -> Task<Message> { | ||
| let mut tasks: Vec<Task<Message>> = Vec::new(); | ||
|
|
||
| for (entity_id, rule) in &self.config.widget_visibility { | ||
| if !self.config.widgets.contains(entity_id) { | ||
| continue; | ||
| } | ||
|
|
||
| let visible = rule.evaluate(&self.ha.entities); | ||
|
|
||
| if visible { | ||
| if !self.is_entity_window_open(entity_id) { | ||
| tracing::debug!(entity = %entity_id, "visibility rule: opening widget"); | ||
| tasks.push(Task::done(Message::OpenEntity(entity_id.clone()))); | ||
| } | ||
| } else if let Some(&id) = self.entity_windows.get(entity_id) { | ||
| tracing::debug!(entity = %entity_id, "visibility rule: closing widget"); | ||
| tasks.push(Task::done(Message::CloseWindow(id))); | ||
| } | ||
| } | ||
|
|
||
| if tasks.is_empty() { | ||
| Task::none() | ||
| } else { | ||
| Task::batch(tasks) | ||
| } | ||
| } | ||
|
|
||
| pub fn update(&mut self, message: Message) -> Task<Message> { | ||
|
|
@@ -978,6 +1057,7 @@ impl Snapdash { | |
| } | ||
|
|
||
| self.entity_windows.insert(entity_id.clone(), id); | ||
| self.entity_windows_opening.remove(entity_id); | ||
| } | ||
|
|
||
| self.windows.insert(id, WindowState { kind, entity }); | ||
|
|
@@ -990,6 +1070,7 @@ impl Snapdash { | |
| && let WindowKind::Entity { entity_id } = window.kind | ||
| { | ||
| self.entity_windows.remove(&entity_id); | ||
| self.entity_windows_opening.remove(&entity_id); | ||
| } | ||
|
|
||
| if self.windows.is_empty() { | ||
|
|
@@ -1063,8 +1144,16 @@ impl Snapdash { | |
| } | ||
|
|
||
| Message::OpenEntity(entity_id) => { | ||
| // Bulk boot-open: skip widgets currently hidden by their rule. | ||
| // At boot HA is not connected, so an gated widget eveluates to "trigger unkonwn" | ||
| // and will open later once HA is connected | ||
| let widgets: Vec<String> = if entity_id.is_empty() { | ||
| self.config.widgets.clone() | ||
| self.config | ||
| .widgets | ||
| .iter() | ||
| .filter(|id| self.is_widget_visible(id)) | ||
| .cloned() | ||
| .collect() | ||
| } else { | ||
| vec![entity_id] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a widget has a saved visibility rule that is currently false, removing and then re-adding it from Settings dispatches Useful? React with 👍 / 👎. |
||
| }; | ||
|
|
@@ -1078,6 +1167,10 @@ impl Snapdash { | |
| continue; | ||
| } | ||
|
|
||
| // Reserve slot immediatelly, so another event fired before our | ||
| // WindowOpened arrives sees the entity as already-being-opened and skips it. | ||
| self.entity_windows_opening.insert(widget.clone()); | ||
|
|
||
| let mut win_settings = window_settings( | ||
| self.config.widget_settings.widget_size.window_size(), | ||
| false, | ||
|
|
@@ -1241,6 +1334,20 @@ impl Snapdash { | |
| kind: WindowKind::ReleaseNotes, | ||
| }) | ||
| } | ||
| Message::OpenWidgetSettings(entity_id) => { | ||
| let kind = WindowKind::WidgetSettings { entity_id }; | ||
|
|
||
| if let Some(opened) = find_window_id(&self.windows, kind.clone(), None) { | ||
| return iced::window::gain_focus::<Message>(opened); | ||
| } | ||
|
|
||
| let settings = window_settings(iced::Size::new(540.0, 540.0), true); | ||
| let (id, task_id) = window::open(settings); | ||
| task_id.map(move |_| Message::WindowOpened { | ||
| id, | ||
| kind: kind.clone(), | ||
| }) | ||
| } | ||
| Message::OpenUrl(url) => { | ||
| if let Err(e) = open::that(&url) { | ||
| tracing::warn!(url, error = %e, "failed to open URL"); | ||
|
|
@@ -1280,6 +1387,73 @@ impl Snapdash { | |
| ) | ||
| } | ||
|
|
||
| Message::WidgetNameChanged(entity_id, raw) => { | ||
| // Empty/whitespace → drop the override so the widget falls | ||
| // back to HA's friendly_name (mirrors the "Normal = drop" | ||
| // pattern from priority — only deviations are persisted). | ||
|
|
||
| if raw.trim().is_empty() { | ||
| self.config.widget_names.remove(&entity_id); | ||
| } else { | ||
| self.config.widget_names.insert(entity_id, raw); | ||
| } | ||
| self.save_config() | ||
| } | ||
|
|
||
| Message::WidgetVisibilityToggled(entity_id, on) => { | ||
| let post_task = if on { | ||
| // Sensible default: self-trigger + IsAvailable — | ||
| // covers "show this sensor only while it reports | ||
| // something real" without forcing the user to pick a | ||
| // trigger entity upfront. | ||
| self.config.widget_visibility.insert( | ||
| entity_id.clone(), | ||
| crate::widget_visibility::VisibilityRule { | ||
| trigger: entity_id, | ||
| condition: crate::widget_visibility::VisibilityCondition::IsAvailable, | ||
| }, | ||
| ); | ||
| self.update_widget_visibility() | ||
| } else { | ||
| self.config.widget_visibility.remove(&entity_id); | ||
| if self.is_entity_window_open(&entity_id) { | ||
| Task::none() | ||
| } else { | ||
| Task::done(Message::OpenEntity(entity_id)) | ||
| } | ||
| }; | ||
|
|
||
| self.save_config().chain(post_task) | ||
| } | ||
|
|
||
| Message::WidgetVisibilityTriggerChanged(entity_id, trigger) => { | ||
| if let Some(rule) = self.config.widget_visibility.get_mut(&entity_id) { | ||
| rule.trigger = trigger; | ||
| } | ||
| self.save_config().chain(self.update_widget_visibility()) | ||
| } | ||
|
|
||
| Message::WidgetVisibilityConditionChanged(entity_id, kind) => { | ||
| if let Some(rule) = self.config.widget_visibility.get_mut(&entity_id) { | ||
| let raw = rule | ||
| .condition | ||
| .raw_value() | ||
| .map(String::from) | ||
| .unwrap_or_default(); | ||
| rule.condition = kind.with_value(raw); | ||
| } | ||
| self.save_config().chain(self.update_widget_visibility()) | ||
| } | ||
|
|
||
| Message::WidgetVisibilityValueChanged(entity_id, raw) => { | ||
| if let Some(rule) = self.config.widget_visibility.get_mut(&entity_id) { | ||
| let kind = | ||
| crate::widget_visibility::ConditionKind::from_condition(&rule.condition); | ||
| rule.condition = kind.with_value(raw); | ||
| } | ||
| self.save_config().chain(self.update_widget_visibility()) | ||
| } | ||
|
|
||
| Message::PersistWidgetPositions => { | ||
| let Some(last) = self.last_widget_move_at else { | ||
| return Task::none(); | ||
|
|
@@ -1336,6 +1510,7 @@ impl Snapdash { | |
| WindowKind::Settings => inner, | ||
| WindowKind::ReleaseNotes => inner, | ||
| WindowKind::ThemeGallery => inner, | ||
| WindowKind::WidgetSettings { .. } => inner, | ||
| }; | ||
|
|
||
| // Platform-specific outer wrapping: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the only open window is an entity widget and its visibility rule becomes false, this new reconciliation path emits
CloseWindow; the existingWindowClosedhandler then seesself.windows.is_empty()and callsiced::exit()at src/app/snapdash.rs:1076. In that single-widget/all-widgets-hidden case the process exits instead of staying connected to HA and reopening the widget when the rule becomes true, so conditional visibility can terminate Snapdash.Useful? React with 👍 / 👎.