aboutsummaryrefslogtreecommitdiff
path: root/src/ui/settings.rs
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-01 15:48:22 +0300
committerGitHub <noreply@github.com>2026-09-01 15:48:22 +0300
commit207be3c771d281baae6e5fa0fb74be9a056e97a2 (patch)
treed7b037ecdf629224a048c841d11659efdc23507d /src/ui/settings.rs
parent8a6d697308162874a236c84aac0b0f8c7ac01f83 (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.rs424
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,
- );
-}