aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app/input/settings.rs73
-rw-r--r--src/app/input/sidebar.rs10
-rw-r--r--src/app/state.rs19
-rw-r--r--src/ui/menus.rs65
-rw-r--r--src/ui/settings.rs15
-rw-r--r--src/ui/sidebar.rs8
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)