diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-01 15:48:22 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-01 15:48:22 +0300 |
| commit | 207be3c771d281baae6e5fa0fb74be9a056e97a2 (patch) | |
| tree | d7b037ecdf629224a048c841d11659efdc23507d /src/ui/settings.rs | |
| parent | 8a6d697308162874a236c84aac0b0f8c7ac01f83 (diff) | |
refactor: render the shell in the client (#3487)
* feat: add opt-in client-rendered shell
* feat: add client-owned shell mouse controls
* feat: complete client-owned shell chrome
* feat: add client-owned shell settings and notifications
* feat: run client shell custom commands
* feat: render popup terminals in client shell
* feat: add client-owned scrollback and copy mode
* fix: preserve client worktree trust defaults
* feat: render startup diagnostics in client shell
* feat: render onboarding in client shell
* feat: render product announcements in client shell
* feat: render release notes in client shell
* feat: render mobile switcher in client shell
* feat: render kitty graphics in client shell
* fix: preserve client shell lifecycle coherence
* refactor: route client shell commands through endpoint connections
* refactor: make the client-rendered shell the default
* refactor: remove monolithic launch mode
* refactor: complete client-rendered shell cutover
* perf: retain client pane surface updates
* perf: scale retained pane surface updates
* perf: remove remote pane input latency
* test: make client shell checks platform-independent
* fix: prevent client timer starvation
* test: fix client shell platform gates
* test: accept retained surfaces in cross-area checks
* test: make client mode assertions platform-neutral
* fix: badge only outdated integrations
Diffstat (limited to 'src/ui/settings.rs')
| -rw-r--r-- | src/ui/settings.rs | 424 |
1 files changed, 0 insertions, 424 deletions
diff --git a/src/ui/settings.rs b/src/ui/settings.rs deleted file mode 100644 index 3cea6da3..00000000 --- a/src/ui/settings.rs +++ /dev/null @@ -1,424 +0,0 @@ -use ratatui::{ - layout::{Constraint, Layout, Rect}, - style::{Modifier, Style}, - text::{Line, Span}, - widgets::{List, ListItem, ListState, Paragraph, Tabs}, - Frame, -}; - -use super::widgets::{ - action_button_row_rects, centered_popup_rect, modal_stack_areas, panel_contrast_fg, - render_action_button, render_modal_choice_list, render_panel_shell, ActionButtonSpec, -}; -use crate::{ - app::{state::Palette, AppState}, - config::{StatusIndicatorStyle, ToastDelivery}, -}; - -pub(crate) const SETTINGS_POPUP_WIDTH: u16 = 76; -pub(crate) const SETTINGS_POPUP_BASE_HEIGHT: u16 = 22; - -pub(crate) fn settings_popup_height(app: &AppState) -> u16 { - if app.settings.section != crate::app::state::SettingsSection::Integrations { - return SETTINGS_POPUP_BASE_HEIGHT; - } - let list_rows = app.integration_recommendations.len().max(1) as u16; - let footer_rows = integrations_footer_height(app, SETTINGS_POPUP_WIDTH - 2); - // borders 2 + header 3 + stack gaps 2 + modal footer 2 - // + section title 1 + description 2 + spacers 2 - (14 + list_rows + footer_rows).max(SETTINGS_POPUP_BASE_HEIGHT) -} - -pub(super) fn render_settings_overlay(app: &AppState, frame: &mut Frame, area: Rect) { - use crate::app::state::SettingsSection; - - let p = &app.palette; - let Some(popup) = centered_popup_rect(area, SETTINGS_POPUP_WIDTH, settings_popup_height(app)) - else { - return; - }; - - super::dim_background(frame, area); - - let Some(inner) = render_panel_shell(frame, popup, p.accent, p.panel_bg) else { - return; - }; - if inner.height < 4 || inner.width < 10 { - return; - } - - let stack = modal_stack_areas(inner, 3, 2, 0, 1); - let header_rows = Layout::vertical([ - Constraint::Length(1), - Constraint::Length(1), - Constraint::Length(1), - ]) - .areas::<3>(stack.header); - - frame.render_widget( - Paragraph::new(Line::from(vec![Span::styled( - " settings", - Style::default().fg(p.text).add_modifier(Modifier::BOLD), - )])), - header_rows[0], - ); - - 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() - .position(|section| *section == app.settings.section) - .unwrap_or(0), - ) - .style(Style::default().fg(p.overlay1)) - .highlight_style( - Style::default() - .fg(panel_contrast_fg(p)) - .bg(p.accent) - .add_modifier(Modifier::BOLD), - ) - .divider(" ") - .padding(" ", " "); - frame.render_widget(tabs, header_rows[1]); - - let sep = "─".repeat(inner.width as usize); - frame.render_widget( - Paragraph::new(Span::styled(&sep, Style::default().fg(p.surface0))), - header_rows[2], - ); - - let content_area = stack.content; - - match app.settings.section { - SettingsSection::Theme => { - render_settings_theme(app, frame, content_area); - } - SettingsSection::Indicators => { - render_modal_choice_list( - frame, - content_area, - "agent status indicators", - "choose color dots or distinct symbols for each state", - &[ - ("color dots ● ● ● ○ ·", StatusIndicatorStyle::Dots), - ("distinct symbols × ◐ ✓ ○ ·", StatusIndicatorStyle::Symbols), - ], - app.status_indicators, - app.settings.list.selected, - p, - 1, - ); - } - SettingsSection::Sound => { - render_settings_toggle( - frame, - content_area, - p, - "sound alerts", - "play sounds when agents change state in background", - app.sound_enabled(), - app.settings.list.selected, - ); - } - SettingsSection::Toast => { - render_modal_choice_list( - frame, - content_area, - "notification popups", - "choose where background popup notifications should appear", - &[ - ("off", ToastDelivery::Off), - ("inside herdr", ToastDelivery::Herdr), - ("via terminal", ToastDelivery::Terminal), - ("via system", ToastDelivery::System), - ], - app.toast_delivery(), - app.settings.list.selected, - p, - 2, - ); - } - SettingsSection::PaneLabels => { - render_settings_toggle( - frame, - content_area, - p, - "agent border labels", - "show detected agent names in split pane borders", - app.agent_border_labels_enabled(), - app.settings.list.selected, - ); - } - SettingsSection::Integrations => { - render_settings_integrations(app, frame, content_area); - } - } - - if let Some(footer_area) = stack.footer { - let footer_rows = Layout::vertical([Constraint::Length(1), Constraint::Length(1)]) - .areas::<2>(footer_area); - let primary_label = settings_primary_button_label(app.settings.section); - let show_primary = settings_show_primary_action(app); - let (apply_rect, close_rect) = - settings_button_rects(inner, app.settings.section, show_primary); - if let Some(apply_rect) = apply_rect { - render_action_button( - frame, - apply_rect, - Some("↵"), - primary_label, - Style::default() - .fg(panel_contrast_fg(p)) - .bg(p.accent) - .add_modifier(Modifier::BOLD), - ); - } - render_action_button( - frame, - close_rect, - Some("esc"), - "close", - Style::default() - .fg(p.text) - .bg(p.surface0) - .add_modifier(Modifier::BOLD), - ); - - frame.render_widget( - Paragraph::new(Line::from(vec![ - Span::styled(" ↑↓", Style::default().fg(p.overlay0)), - Span::styled(" select ", Style::default().fg(p.overlay1)), - Span::styled("tab", Style::default().fg(p.overlay0)), - Span::styled(" section", Style::default().fg(p.overlay1)), - ])), - footer_rows[0], - ); - } -} - -pub(crate) fn settings_primary_button_label( - section: crate::app::state::SettingsSection, -) -> &'static str { - match section { - crate::app::state::SettingsSection::Integrations => "install", - _ => "apply", - } -} - -pub(crate) fn settings_show_primary_action(app: &AppState) -> bool { - match app.settings.section { - crate::app::state::SettingsSection::Integrations => app - .integration_recommendations - .iter() - .any(crate::integration::IntegrationRecommendation::needs_install), - _ => true, - } -} - -pub(crate) fn settings_button_rects( - inner: Rect, - section: crate::app::state::SettingsSection, - show_primary: bool, -) -> (Option<Rect>, Rect) { - if !show_primary { - let rects = action_button_row_rects( - inner, - &[ActionButtonSpec { - hint: Some("esc"), - label: "close", - }], - 2, - inner.height.saturating_sub(1), - ); - return (None, rects[0]); - } - - let rects = action_button_row_rects( - inner, - &[ - ActionButtonSpec { - hint: Some("↵"), - label: settings_primary_button_label(section), - }, - ActionButtonSpec { - hint: Some("esc"), - label: "close", - }, - ], - 2, - inner.height.saturating_sub(1), - ); - (Some(rects[0]), rects[1]) -} - -fn integrations_footer_paragraph(app: &AppState) -> Paragraph<'static> { - let p = &app.palette; - let mut footer_lines = Vec::new(); - if !app.integration_install_messages.is_empty() { - for message in &app.integration_install_messages { - footer_lines.push(Line::from(Span::styled( - format!(" {message}"), - Style::default().fg(p.overlay1), - ))); - } - } else { - let found_any = app.integration_recommendations.iter().any(|item| { - item.available || item.state != crate::integration::IntegrationStatusKind::NotInstalled - }); - let hint = if app - .integration_recommendations - .iter() - .any(crate::integration::IntegrationRecommendation::needs_install) - { - " press install to add available or outdated integrations" - } else if found_any { - " all detected integrations are installed" - } else { - " no supported agent CLIs found on PATH" - }; - footer_lines.push(Line::from(Span::styled( - hint.to_string(), - Style::default().fg(p.overlay1), - ))); - } - Paragraph::new(footer_lines).wrap(ratatui::widgets::Wrap { trim: false }) -} - -fn integrations_footer_height(app: &AppState, width: u16) -> u16 { - (integrations_footer_paragraph(app).line_count(width) as u16).min(6) -} - -fn render_settings_integrations(app: &AppState, frame: &mut Frame, area: Rect) { - let p = &app.palette; - - let footer = integrations_footer_paragraph(app); - let footer_height = integrations_footer_height(app, area.width); - - let rows = Layout::vertical([ - Constraint::Length(1), - Constraint::Length(2), - Constraint::Length(1), - Constraint::Min(0), - Constraint::Length(1), - Constraint::Length(footer_height), - ]) - .areas::<6>(area); - - frame.render_widget( - Paragraph::new("agent integrations") - .style(Style::default().fg(p.text).add_modifier(Modifier::BOLD)), - rows[0], - ); - frame.render_widget( - Paragraph::new( - "let agents report state directly instead of relying only on process detection", - ) - .style(Style::default().fg(p.overlay1)) - .wrap(ratatui::widgets::Wrap { trim: false }), - rows[1], - ); - - let mut lines = Vec::new(); - for item in &app.integration_recommendations { - let marker = match item.state { - crate::integration::IntegrationStatusKind::Current => "✓", - crate::integration::IntegrationStatusKind::Outdated => "↻", - crate::integration::IntegrationStatusKind::NotInstalled if item.available => "+", - crate::integration::IntegrationStatusKind::NotInstalled => "–", - }; - let marker_style = match item.state { - crate::integration::IntegrationStatusKind::Current => Style::default().fg(p.green), - crate::integration::IntegrationStatusKind::Outdated => Style::default().fg(p.yellow), - crate::integration::IntegrationStatusKind::NotInstalled if item.available => { - Style::default().fg(p.accent) - } - crate::integration::IntegrationStatusKind::NotInstalled => { - Style::default().fg(p.overlay0) - } - }; - lines.push(Line::from(vec![ - Span::styled(format!(" {marker} "), marker_style), - Span::styled( - format!("{:<9}", item.label), - Style::default().fg(p.subtext0), - ), - Span::styled(item.status_label(), Style::default().fg(p.overlay1)), - ])); - } - - if lines.is_empty() { - lines.push(Line::from(Span::styled( - " no integration targets available", - Style::default().fg(p.overlay1), - ))); - } - - frame.render_widget(Paragraph::new(lines), rows[3]); - frame.render_widget(footer, rows[5]); -} - -fn render_settings_theme(app: &AppState, frame: &mut Frame, area: Rect) { - use crate::app::state::THEME_NAMES; - - let p = &app.palette; - let items: Vec<ListItem> = THEME_NAMES - .iter() - .map(|name| { - let is_current = name.to_lowercase().replace([' ', '_'], "-") - == app.theme_name.to_lowercase().replace([' ', '_'], "-"); - let marker = if is_current { " ✓" } else { "" }; - ListItem::new(Line::from(vec![ - Span::styled(*name, Style::default().fg(p.subtext0)), - Span::styled(marker, Style::default().fg(p.green)), - ])) - }) - .collect(); - - let list = List::new(items) - .highlight_style( - Style::default() - .bg(p.surface0) - .fg(p.text) - .add_modifier(Modifier::BOLD), - ) - .highlight_symbol(" ▸ ") - .style(Style::default().fg(p.subtext0)); - - let mut state = ListState::default().with_selected(Some(app.settings.list.selected)); - frame.render_stateful_widget(list, area, &mut state); -} - -fn render_settings_toggle( - frame: &mut Frame, - area: Rect, - p: &Palette, - title: &str, - description: &str, - current_value: bool, - selected_idx: usize, -) { - render_modal_choice_list( - frame, - area, - title, - description, - &[("on", true), ("off", false)], - current_value, - selected_idx, - p, - 1, - ); -} |
