diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/input/settings.rs | 73 | ||||
| -rw-r--r-- | src/app/input/sidebar.rs | 10 | ||||
| -rw-r--r-- | src/app/state.rs | 19 | ||||
| -rw-r--r-- | src/ui/menus.rs | 65 | ||||
| -rw-r--r-- | src/ui/settings.rs | 15 | ||||
| -rw-r--r-- | src/ui/sidebar.rs | 8 |
6 files changed, 138 insertions, 52 deletions
diff --git a/src/app/input/settings.rs b/src/app/input/settings.rs index 400b2796..6caccb0e 100644 --- a/src/app/input/settings.rs +++ b/src/app/input/settings.rs @@ -283,7 +283,12 @@ impl AppState { } let mut x = inner.x; for section in SettingsSection::ALL { - let width = section.label().len() as u16 + 2; + let badge_width = if self.settings_section_has_badge(*section) { + 2 + } else { + 0 + }; + let width = section.label().len() as u16 + 2 + badge_width; if col >= x && col < x + width { return Some(*section); } @@ -493,4 +498,70 @@ mod tests { assert_eq!(app.state.settings.list.selected, 0); } + + #[test] + fn integration_update_badge_only_tracks_outdated_recommendations() { + let mut state = state_with_workspaces(&["test"]); + state.integration_recommendations = vec![integration_recommendation( + crate::integration::IntegrationStatusKind::NotInstalled, + true, + )]; + assert!(!state.integration_updates_available()); + + state.integration_recommendations = vec![integration_recommendation( + crate::integration::IntegrationStatusKind::NotInstalled, + false, + )]; + assert!(!state.integration_updates_available()); + + state.integration_recommendations = vec![integration_recommendation( + crate::integration::IntegrationStatusKind::Current, + true, + )]; + assert!(!state.integration_updates_available()); + + state.integration_recommendations = vec![integration_recommendation( + crate::integration::IntegrationStatusKind::Outdated, + true, + )]; + assert!(state.integration_updates_available()); + } + + #[test] + fn settings_tab_hit_area_includes_integration_update_badge() { + let mut state = state_with_workspaces(&["test"]); + state.integration_recommendations = vec![integration_recommendation( + crate::integration::IntegrationStatusKind::Outdated, + true, + )]; + open_settings(&mut state); + + let inner = state.settings_inner_rect(); + let tab_y = inner.y + 1; + let integrations_x = inner.x + + SettingsSection::ALL[..SettingsSection::ALL.len() - 1] + .iter() + .map(|section| section.label().len() as u16 + 3) + .sum::<u16>(); + let dotted_width = SettingsSection::Integrations.label().len() as u16 + 4; + + assert_eq!( + state.settings_tab_at(integrations_x + dotted_width - 1, tab_y), + Some(SettingsSection::Integrations) + ); + } + + fn integration_recommendation( + state: crate::integration::IntegrationStatusKind, + available: bool, + ) -> crate::integration::IntegrationRecommendation { + crate::integration::IntegrationRecommendation { + target: crate::api::schema::IntegrationTarget::Claude, + label: "claude", + command: "claude", + available, + path: std::path::PathBuf::from("/tmp/herdr-test-integration"), + state, + } + } } diff --git a/src/app/input/sidebar.rs b/src/app/input/sidebar.rs index 94feaf93..7491c9c1 100644 --- a/src/app/input/sidebar.rs +++ b/src/app/input/sidebar.rs @@ -175,7 +175,7 @@ impl AppState { } let footer = self.sidebar_footer_rect(); - let width = if self.update_available.is_some() { + let width = if self.global_menu_attention_badge_visible() { 8 } else { 6 @@ -203,8 +203,12 @@ impl AppState { let content_width = labels .iter() .map(|label| { - let extra = if *label == "update ready" { 2 } else { 0 }; - label.chars().count() as u16 + extra + let badge_width = if self.global_menu_item_has_badge(label) { + 2 + } else { + 0 + }; + label.chars().count() as u16 + badge_width }) .max() .unwrap_or(8) diff --git a/src/app/state.rs b/src/app/state.rs index 2b078caf..921a82c9 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -1011,6 +1011,25 @@ impl AppState { self.show_agent_labels_on_pane_borders } + pub(crate) fn integration_updates_available(&self) -> bool { + self.integration_recommendations + .iter() + .any(|item| item.state == crate::integration::IntegrationStatusKind::Outdated) + } + + pub(crate) fn global_menu_attention_badge_visible(&self) -> bool { + self.update_available.is_some() || self.integration_updates_available() + } + + pub(crate) fn global_menu_item_has_badge(&self, item: &str) -> bool { + (item == "update ready" && self.update_available.is_some()) + || (item == "settings" && self.integration_updates_available()) + } + + pub(crate) fn settings_section_has_badge(&self, section: SettingsSection) -> bool { + section == SettingsSection::Integrations && self.integration_updates_available() + } + pub fn focused_pane_requests_mouse_capture(&self) -> bool { self.mode == Mode::Terminal && self diff --git a/src/ui/menus.rs b/src/ui/menus.rs index d17aa897..aa5e3b79 100644 --- a/src/ui/menus.rs +++ b/src/ui/menus.rs @@ -148,53 +148,32 @@ pub(super) fn render_global_launcher_menu(app: &AppState, frame: &mut Frame) { let selected = idx == app.global_menu.highlighted; let rect = Rect::new(inner.x, y, inner.width, 1); - if *item == "update ready" { - let line = if selected { - Line::from(vec![ - Span::styled( - " update ready ", - Style::default() - .fg(panel_contrast_fg(&app.palette)) - .bg(app.palette.accent) - .add_modifier(Modifier::BOLD), - ), - Span::styled( - "● ", - Style::default() - .fg(panel_contrast_fg(&app.palette)) - .bg(app.palette.accent) - .add_modifier(Modifier::BOLD), - ), - ]) - } else { - Line::from(vec![ - Span::styled(" update ready ", Style::default().fg(app.palette.text)), - Span::styled( - "● ", - Style::default() - .fg(app.palette.accent) - .add_modifier(Modifier::BOLD), - ), - ]) - }; - frame.render_widget(Paragraph::new(line).alignment(Alignment::Left), rect); - continue; - } - - let style = if selected { + let selected_style = Style::default() + .fg(panel_contrast_fg(&app.palette)) + .bg(app.palette.accent) + .add_modifier(Modifier::BOLD); + let item_style = if selected { + selected_style + } else { + Style::default().fg(app.palette.text) + }; + let badge_style = if selected { + selected_style + } else { Style::default() - .fg(panel_contrast_fg(&app.palette)) - .bg(app.palette.accent) + .fg(app.palette.accent) .add_modifier(Modifier::BOLD) + }; + + let line = if app.global_menu_item_has_badge(item) { + Line::from(vec![ + Span::styled(" ●", badge_style), + Span::styled(format!(" {item} "), item_style), + ]) } else { - Style::default().fg(app.palette.text) + Line::from(Span::styled(format!(" {item} "), item_style)) }; - frame.render_widget( - Paragraph::new(format!(" {item} ")) - .style(style) - .alignment(Alignment::Left), - rect, - ); + frame.render_widget(Paragraph::new(line).alignment(Alignment::Left), rect); } } diff --git a/src/ui/settings.rs b/src/ui/settings.rs index c191eef2..f104c882 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -48,7 +48,20 @@ pub(super) fn render_settings_overlay(app: &AppState, frame: &mut Frame, area: R header_rows[0], ); - let tabs = Tabs::new(SettingsSection::ALL.iter().map(|s| s.label())) + let tab_labels = SettingsSection::ALL.iter().map(|section| { + if app.settings_section_has_badge(*section) { + Line::from(vec![ + Span::styled( + "● ", + Style::default().fg(p.accent).add_modifier(Modifier::BOLD), + ), + Span::raw(section.label()), + ]) + } else { + Line::from(section.label()) + } + }); + let tabs = Tabs::new(tab_labels) .select( SettingsSection::ALL .iter() diff --git a/src/ui/sidebar.rs b/src/ui/sidebar.rs index b6b5bc9e..1d722d2a 100644 --- a/src/ui/sidebar.rs +++ b/src/ui/sidebar.rs @@ -731,13 +731,13 @@ fn render_workspace_list(app: &AppState, frame: &mut Frame, area: Rect, is_navig ); let menu_rect = app.global_launcher_rect(); - let menu_line = if app.update_available.is_some() { + let menu_line = if app.global_menu_attention_badge_visible() { Line::from(vec![ - Span::styled("menu", Style::default().fg(p.overlay0)), Span::styled( - " ●", + "● ", Style::default().fg(p.accent).add_modifier(Modifier::BOLD), ), + Span::styled("menu", Style::default().fg(p.overlay0)), ]) } else { Line::from(vec![Span::styled("menu", Style::default().fg(p.overlay0))]) @@ -887,7 +887,7 @@ fn render_sidebar_toggle( if toggle_area == Rect::default() { return; } - let icon_style = if app.update_available.is_some() { + let icon_style = if app.global_menu_attention_badge_visible() { Style::default().fg(p.accent).add_modifier(Modifier::BOLD) } else { Style::default().fg(p.overlay0) |
