diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-08-04 17:26:30 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-04 17:26:30 +0300 |
| commit | cc9fa47540d8525f24fe46a10f759ca40090a6e0 (patch) | |
| tree | ef6979a7d5168952dc2de8d7a68e42b3ad69c45b /src/app | |
| parent | 4473e391962c68d59bd980fa98a44f3397d9931d (diff) | |
feat(ui): add distinct status indicators (#2282)
refs #2260
Diffstat (limited to 'src/app')
| -rw-r--r-- | src/app/config_io.rs | 13 | ||||
| -rw-r--r-- | src/app/input/mod.rs | 3 | ||||
| -rw-r--r-- | src/app/input/settings.rs | 83 | ||||
| -rw-r--r-- | src/app/mod.rs | 30 | ||||
| -rw-r--r-- | src/app/state.rs | 5 |
5 files changed, 127 insertions, 7 deletions
diff --git a/src/app/config_io.rs b/src/app/config_io.rs index 59077b6f..9f7445b9 100644 --- a/src/app/config_io.rs +++ b/src/app/config_io.rs @@ -55,6 +55,19 @@ impl App { } } + pub(super) fn save_status_indicators(&mut self, style: crate::config::StatusIndicatorStyle) { + if self.update_config_file("status indicators", |content| { + crate::config::upsert_section_value( + content, + "ui", + "status_indicators", + &format!("\"{}\"", style.as_str()), + ) + }) { + self.apply_config_from_disk(false); + } + } + pub(super) fn save_sound(&mut self, enabled: bool) { if self.update_config_file("sound setting", |content| { crate::config::upsert_section_bool(content, "ui.sound", "enabled", enabled) diff --git a/src/app/input/mod.rs b/src/app/input/mod.rs index 7eca40b6..f77821c9 100644 --- a/src/app/input/mod.rs +++ b/src/app/input/mod.rs @@ -399,6 +399,9 @@ impl App { } MouseAction::Settings(action) => match action { SettingsAction::SaveTheme(name) => self.save_theme(&name), + SettingsAction::SaveStatusIndicators(style) => { + self.save_status_indicators(style) + } SettingsAction::SaveSound(enabled) => self.save_sound(enabled), SettingsAction::SaveToastDelivery(delivery) => { self.save_toast_delivery(delivery) diff --git a/src/app/input/settings.rs b/src/app/input/settings.rs index 326d841b..fb1a7d75 100644 --- a/src/app/input/settings.rs +++ b/src/app/input/settings.rs @@ -6,7 +6,7 @@ use crate::{ state::{AppState, SettingsSection, THEME_NAMES}, App, Mode, }, - config::ToastDelivery, + config::{StatusIndicatorStyle, ToastDelivery}, }; #[derive(Debug, Clone, PartialEq, Eq)] @@ -14,6 +14,7 @@ use crate::{ #[allow(clippy::enum_variant_names)] pub(super) enum SettingsAction { SaveTheme(String), + SaveStatusIndicators(StatusIndicatorStyle), SaveSound(bool), SaveToastDelivery(ToastDelivery), SaveAgentBorderLabels(bool), @@ -26,6 +27,7 @@ impl App { if let Some(action) = update_settings_state(&mut self.state, key) { match action { SettingsAction::SaveTheme(name) => self.save_theme(&name), + SettingsAction::SaveStatusIndicators(style) => self.save_status_indicators(style), SettingsAction::SaveSound(enabled) => self.save_sound(enabled), SettingsAction::SaveToastDelivery(delivery) => self.save_toast_delivery(delivery), SettingsAction::SaveAgentBorderLabels(enabled) => { @@ -56,6 +58,21 @@ fn current_theme_index(theme_name: &str) -> usize { .unwrap_or(0) } +fn status_indicator_index(style: StatusIndicatorStyle) -> usize { + match style { + StatusIndicatorStyle::Dots => 0, + StatusIndicatorStyle::Symbols => 1, + } +} + +fn status_indicator_for_index(idx: usize) -> StatusIndicatorStyle { + if idx == 0 { + StatusIndicatorStyle::Dots + } else { + StatusIndicatorStyle::Symbols + } +} + fn toast_delivery_index(delivery: ToastDelivery) -> usize { match delivery { ToastDelivery::Off => 0, @@ -145,8 +162,8 @@ pub(super) fn update_settings_state(state: &mut AppState, key: KeyEvent) -> Opti } } KeyCode::Tab | KeyCode::Right | KeyCode::Char('l') => { - state.settings.section = SettingsSection::Sound; - state.settings.list.selected = usize::from(!state.sound_enabled()); + state.settings.section = SettingsSection::Indicators; + state.settings.list.selected = status_indicator_index(state.status_indicators); } KeyCode::BackTab | KeyCode::Left | KeyCode::Char('h') => { state.settings.section = SettingsSection::Integrations; @@ -158,6 +175,30 @@ pub(super) fn update_settings_state(state: &mut AppState, key: KeyEvent) -> Opti _ => {} }, }, + SettingsSection::Indicators => match key.code { + KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => { + state.settings.list.selected = 1 - state.settings.list.selected.min(1); + } + KeyCode::Enter | KeyCode::Char(' ') => { + let style = status_indicator_for_index(state.settings.list.selected); + return Some(SettingsAction::SaveStatusIndicators(style)); + } + KeyCode::BackTab | KeyCode::Left | KeyCode::Char('h') => { + state.settings.section = SettingsSection::Theme; + state.settings.list.selected = current_theme_index(&state.theme_name); + } + KeyCode::Tab | KeyCode::Right | KeyCode::Char('l') => { + state.settings.section = SettingsSection::Sound; + state.settings.list.selected = usize::from(!state.sound_enabled()); + } + _ => { + if let Some(super::modal::ModalAction::Close) = + super::modal::modal_action_from_key(&key, super::modal::SETTINGS_ACTIONS) + { + cancel_settings(state); + } + } + }, SettingsSection::Sound => match key.code { KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => { state.settings.list.selected = 1 - state.settings.list.selected.min(1); @@ -171,8 +212,8 @@ pub(super) fn update_settings_state(state: &mut AppState, key: KeyEvent) -> Opti state.settings.list.selected = toast_delivery_index(state.toast_delivery()); } KeyCode::BackTab | KeyCode::Left | KeyCode::Char('h') => { - state.settings.section = SettingsSection::Theme; - state.settings.list.selected = current_theme_index(&state.theme_name); + state.settings.section = SettingsSection::Indicators; + state.settings.list.selected = status_indicator_index(state.status_indicators); } _ => { if let Some(super::modal::ModalAction::Close) = @@ -263,6 +304,7 @@ pub(crate) fn open_settings_at(state: &mut AppState, section: SettingsSection) { state.settings.section = section; state.settings.list.selected = match section { SettingsSection::Theme => current_theme_index(&state.theme_name), + SettingsSection::Indicators => status_indicator_index(state.status_indicators), SettingsSection::Sound => usize::from(!state.sound_enabled()), SettingsSection::Toast => toast_delivery_index(state.toast_delivery()), SettingsSection::PaneLabels => usize::from(!state.agent_border_labels_enabled()), @@ -336,7 +378,7 @@ impl AppState { let idx = scroll + (row - area.y) as usize; (idx < THEME_NAMES.len()).then_some(idx) } - SettingsSection::Sound => { + SettingsSection::Indicators | SettingsSection::Sound => { let list_y = area.y + 3; if row >= list_y && row < list_y + 2 { Some((row - list_y) as usize) @@ -371,6 +413,9 @@ impl AppState { self.settings.section = section; self.settings.list.select(match section { SettingsSection::Theme => current_theme_index(&self.theme_name), + SettingsSection::Indicators => { + status_indicator_index(self.status_indicators) + } SettingsSection::Sound => usize::from(!self.sound_enabled()), SettingsSection::Toast => toast_delivery_index(self.toast_delivery()), SettingsSection::PaneLabels => { @@ -387,6 +432,9 @@ impl AppState { preview_selected_theme(self); None } + SettingsSection::Indicators => Some(SettingsAction::SaveStatusIndicators( + status_indicator_for_index(idx), + )), SettingsSection::Sound => { let enabled = idx == 0; Some(SettingsAction::SaveSound(enabled)) @@ -454,7 +502,7 @@ mod tests { ); assert_eq!( state.settings.section, - crate::app::state::SettingsSection::Sound + crate::app::state::SettingsSection::Indicators ); update_settings_state( @@ -469,6 +517,27 @@ mod tests { } #[test] + fn settings_indicator_choice_returns_save_action() { + let mut state = state_with_workspaces(&["test"]); + open_settings_at(&mut state, SettingsSection::Indicators); + state.settings.list.selected = 1; + + let action = update_settings_state( + &mut state, + KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()), + ); + + assert_eq!( + action, + Some(SettingsAction::SaveStatusIndicators( + StatusIndicatorStyle::Symbols + )) + ); + assert_eq!(state.status_indicators, StatusIndicatorStyle::Dots); + assert_eq!(state.mode, Mode::Settings); + } + + #[test] fn settings_sound_toggle_returns_save_action() { let mut state = state_with_workspaces(&["test"]); open_settings(&mut state); diff --git a/src/app/mod.rs b/src/app/mod.rs index d049c0f2..82ac1a60 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -616,6 +616,7 @@ impl App { sidebar_collapsed_mode: config.ui.sidebar_collapsed_mode, sidebar_section_split, agent_panel_sort, + status_indicators: config.ui.status_indicators, agent_view_override: None, sidebar_agents: config.ui.sidebar.agents.clone(), sidebar_spaces: config.ui.sidebar.spaces.clone(), @@ -1446,6 +1447,7 @@ impl App { self.state.tab_bar_position = config.ui.tab_bar_position; self.state.agent_panel_sort = agent_panel_sort_from_config(config.ui.agent_panel_sort); + self.state.status_indicators = config.ui.status_indicators; self.state.sidebar_agents = config.ui.sidebar.agents.clone(); self.state.sidebar_spaces = config.ui.sidebar.spaces.clone(); self.state.agent_panel_scroll = 0; @@ -3443,6 +3445,34 @@ mod tests { } #[test] + fn save_status_indicators_persists_then_applies_live_config() { + let _guard = config_env_lock().lock().unwrap(); + let path = temp_config_path("save-status-indicators"); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write(&path, "onboarding = false\n").unwrap(); + std::env::set_var(crate::config::CONFIG_PATH_ENV_VAR, &path); + + let mut app = test_app(); + assert_eq!( + app.state.status_indicators, + crate::config::StatusIndicatorStyle::Dots + ); + + app.save_status_indicators(crate::config::StatusIndicatorStyle::Symbols); + + assert_eq!( + app.state.status_indicators, + crate::config::StatusIndicatorStyle::Symbols + ); + let content = std::fs::read_to_string(&path).unwrap(); + assert!(content.contains("status_indicators = \"symbols\"")); + assert!(app.state.config_diagnostic.is_none()); + + std::env::remove_var(crate::config::CONFIG_PATH_ENV_VAR); + let _ = std::fs::remove_dir_all(path.parent().unwrap()); + } + + #[test] fn save_agent_panel_sort_persists_then_applies_live_config() { let _guard = config_env_lock().lock().unwrap(); let path = temp_config_path("save-agent-panel-sort"); diff --git a/src/app/state.rs b/src/app/state.rs index e6c23c4c..7a8837ee 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -991,6 +991,7 @@ pub enum AgentPanelSort { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SettingsSection { Theme, + Indicators, Sound, Toast, PaneLabels, @@ -1000,6 +1001,7 @@ pub enum SettingsSection { impl SettingsSection { pub const ALL: &[Self] = &[ Self::Theme, + Self::Indicators, Self::Sound, Self::Toast, Self::PaneLabels, @@ -1009,6 +1011,7 @@ impl SettingsSection { pub fn label(self) -> &'static str { match self { Self::Theme => "theme", + Self::Indicators => "indicators", Self::Sound => "sound", Self::Toast => "toasts", Self::PaneLabels => "pane labels", @@ -1476,6 +1479,7 @@ pub struct AppState { /// Ratio of sidebar height allocated to the workspaces section. pub sidebar_section_split: f32, pub agent_panel_sort: AgentPanelSort, + pub status_indicators: crate::config::StatusIndicatorStyle, /// Transient session-wide projection override for the built-in Agents view. pub agent_view_override: Option<crate::api::schema::AgentViewSetParams>, pub sidebar_agents: crate::config::AgentsSidebarConfig, @@ -1847,6 +1851,7 @@ impl AppState { sidebar_collapsed_mode: crate::config::SidebarCollapsedModeConfig::Compact, sidebar_section_split: 0.5, agent_panel_sort: AgentPanelSort::Spaces, + status_indicators: crate::config::StatusIndicatorStyle::Dots, agent_view_override: None, sidebar_agents: crate::config::AgentsSidebarConfig::default(), sidebar_spaces: crate::config::SpacesSidebarConfig::default(), |
