aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-08-04 17:26:30 +0300
committerGitHub <noreply@github.com>2026-08-04 17:26:30 +0300
commitcc9fa47540d8525f24fe46a10f759ca40090a6e0 (patch)
treeef6979a7d5168952dc2de8d7a68e42b3ad69c45b /src/app
parent4473e391962c68d59bd980fa98a44f3397d9931d (diff)
feat(ui): add distinct status indicators (#2282)
refs #2260
Diffstat (limited to 'src/app')
-rw-r--r--src/app/config_io.rs13
-rw-r--r--src/app/input/mod.rs3
-rw-r--r--src/app/input/settings.rs83
-rw-r--r--src/app/mod.rs30
-rw-r--r--src/app/state.rs5
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(),