mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-13 21:33:32 +08:00
feat: add ecc2 runtime theme toggle
This commit is contained in:
@@ -40,7 +40,7 @@ pub struct Config {
|
|||||||
pub risk_thresholds: RiskThresholds,
|
pub risk_thresholds: RiskThresholds,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub enum Theme {
|
pub enum Theme {
|
||||||
Dark,
|
Dark,
|
||||||
Light,
|
Light,
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ pub async fn run(db: StateStore, cfg: Config) -> Result<()> {
|
|||||||
(_, KeyCode::Char('m')) => dashboard.merge_selected_worktree().await,
|
(_, KeyCode::Char('m')) => dashboard.merge_selected_worktree().await,
|
||||||
(_, KeyCode::Char('M')) => dashboard.merge_ready_worktrees().await,
|
(_, KeyCode::Char('M')) => dashboard.merge_ready_worktrees().await,
|
||||||
(_, KeyCode::Char('l')) => dashboard.cycle_pane_layout(),
|
(_, KeyCode::Char('l')) => dashboard.cycle_pane_layout(),
|
||||||
|
(_, KeyCode::Char('T')) => dashboard.toggle_theme(),
|
||||||
(_, KeyCode::Char('p')) => dashboard.toggle_auto_dispatch_policy(),
|
(_, KeyCode::Char('p')) => dashboard.toggle_auto_dispatch_policy(),
|
||||||
(_, KeyCode::Char('t')) => dashboard.toggle_auto_worktree_policy(),
|
(_, KeyCode::Char('t')) => dashboard.toggle_auto_worktree_policy(),
|
||||||
(_, KeyCode::Char('w')) => dashboard.toggle_auto_merge_policy(),
|
(_, KeyCode::Char('w')) => dashboard.toggle_auto_merge_policy(),
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use tokio::sync::broadcast;
|
|||||||
|
|
||||||
use super::widgets::{budget_state, format_currency, format_token_count, BudgetState, TokenMeter};
|
use super::widgets::{budget_state, format_currency, format_token_count, BudgetState, TokenMeter};
|
||||||
use crate::comms;
|
use crate::comms;
|
||||||
use crate::config::{Config, PaneLayout};
|
use crate::config::{Config, PaneLayout, Theme};
|
||||||
use crate::observability::ToolLogEntry;
|
use crate::observability::ToolLogEntry;
|
||||||
use crate::session::manager;
|
use crate::session::manager;
|
||||||
use crate::session::output::{OutputEvent, OutputLine, SessionOutputStore, OUTPUT_BUFFER_LIMIT};
|
use crate::session::output::{OutputEvent, OutputLine, SessionOutputStore, OUTPUT_BUFFER_LIMIT};
|
||||||
@@ -45,6 +45,14 @@ struct WorktreeDiffColumns {
|
|||||||
additions: String,
|
additions: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct ThemePalette {
|
||||||
|
accent: Color,
|
||||||
|
row_highlight_bg: Color,
|
||||||
|
muted: Color,
|
||||||
|
help_border: Color,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Dashboard {
|
pub struct Dashboard {
|
||||||
db: StateStore,
|
db: StateStore,
|
||||||
cfg: Config,
|
cfg: Config,
|
||||||
@@ -244,11 +252,13 @@ impl Dashboard {
|
|||||||
.filter(|session| session.state == SessionState::Running)
|
.filter(|session| session.state == SessionState::Running)
|
||||||
.count();
|
.count();
|
||||||
let total = self.sessions.len();
|
let total = self.sessions.len();
|
||||||
|
let palette = self.theme_palette();
|
||||||
|
|
||||||
let title = format!(
|
let title = format!(
|
||||||
" ECC 2.0 | {running} running / {total} total | {} {}% ",
|
" ECC 2.0 | {running} running / {total} total | {} {}% | {} ",
|
||||||
self.layout_label(),
|
self.layout_label(),
|
||||||
self.pane_size_percent
|
self.pane_size_percent,
|
||||||
|
self.theme_label()
|
||||||
);
|
);
|
||||||
let tabs = Tabs::new(
|
let tabs = Tabs::new(
|
||||||
self.visible_panes()
|
self.visible_panes()
|
||||||
@@ -260,7 +270,7 @@ impl Dashboard {
|
|||||||
.select(self.selected_pane_index())
|
.select(self.selected_pane_index())
|
||||||
.highlight_style(
|
.highlight_style(
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::Cyan)
|
.fg(palette.accent)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -332,7 +342,7 @@ impl Dashboard {
|
|||||||
.highlight_spacing(HighlightSpacing::Always)
|
.highlight_spacing(HighlightSpacing::Always)
|
||||||
.row_highlight_style(
|
.row_highlight_style(
|
||||||
Style::default()
|
Style::default()
|
||||||
.bg(Color::DarkGray)
|
.bg(self.theme_palette().row_highlight_bg)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -530,8 +540,9 @@ impl Dashboard {
|
|||||||
|
|
||||||
fn render_status_bar(&self, frame: &mut Frame, area: Rect) {
|
fn render_status_bar(&self, frame: &mut Frame, area: Rect) {
|
||||||
let text = format!(
|
let text = format!(
|
||||||
" [n]ew session [a]ssign re[b]alance global re[B]alance dra[i]n inbox [g]lobal dispatch coordinate [G]lobal [v]iew diff conflict proto[c]ol [m]erge merge ready [M] auto-worktree [t] auto-merge [w] toggle [p]olicy [,/.] dispatch limit [s]top [u]resume [x]cleanup prune inactive [X] [d]elete [r]efresh [Tab] switch pane [j/k] scroll [+/-] resize [l]ayout {} [?] help [q]uit ",
|
" [n]ew session [a]ssign re[b]alance global re[B]alance dra[i]n inbox [g]lobal dispatch coordinate [G]lobal [v]iew diff conflict proto[c]ol [m]erge merge ready [M] auto-worktree [t] auto-merge [w] toggle [p]olicy [,/.] dispatch limit [s]top [u]resume [x]cleanup prune inactive [X] [d]elete [r]efresh [Tab] switch pane [j/k] scroll [+/-] resize [l]ayout {} [T]heme {} [?] help [q]uit ",
|
||||||
self.layout_label()
|
self.layout_label(),
|
||||||
|
self.theme_label()
|
||||||
);
|
);
|
||||||
let text = if let Some(note) = self.operator_note.as_ref() {
|
let text = if let Some(note) = self.operator_note.as_ref() {
|
||||||
format!(" {} |{}", truncate_for_dashboard(note, 96), text)
|
format!(" {} |{}", truncate_for_dashboard(note, 96), text)
|
||||||
@@ -559,7 +570,7 @@ impl Dashboard {
|
|||||||
.split(inner);
|
.split(inner);
|
||||||
|
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(text).style(Style::default().fg(Color::DarkGray)),
|
Paragraph::new(text).style(Style::default().fg(self.theme_palette().muted)),
|
||||||
chunks[0],
|
chunks[0],
|
||||||
);
|
);
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
@@ -586,6 +597,7 @@ impl Dashboard {
|
|||||||
" m Merge selected ready worktree into base and clean it up",
|
" m Merge selected ready worktree into base and clean it up",
|
||||||
" M Merge all ready inactive worktrees and clean them up",
|
" M Merge all ready inactive worktrees and clean them up",
|
||||||
" l Cycle pane layout and persist it",
|
" l Cycle pane layout and persist it",
|
||||||
|
" T Toggle theme and persist it",
|
||||||
" t Toggle default worktree creation for new sessions and delegated work",
|
" t Toggle default worktree creation for new sessions and delegated work",
|
||||||
" p Toggle daemon auto-dispatch policy and persist config",
|
" p Toggle daemon auto-dispatch policy and persist config",
|
||||||
" w Toggle daemon auto-merge for ready inactive worktrees",
|
" w Toggle daemon auto-merge for ready inactive worktrees",
|
||||||
@@ -610,7 +622,7 @@ impl Dashboard {
|
|||||||
Block::default()
|
Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.title(" Help ")
|
.title(" Help ")
|
||||||
.border_style(Style::default().fg(Color::Yellow)),
|
.border_style(Style::default().fg(self.theme_palette().help_border)),
|
||||||
);
|
);
|
||||||
frame.render_widget(paragraph, area);
|
frame.render_widget(paragraph, area);
|
||||||
}
|
}
|
||||||
@@ -673,6 +685,34 @@ impl Dashboard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn toggle_theme(&mut self) {
|
||||||
|
let config_path = crate::config::Config::config_path();
|
||||||
|
self.toggle_theme_with_save(&config_path, |cfg| cfg.save());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_theme_with_save<F>(&mut self, config_path: &std::path::Path, save: F)
|
||||||
|
where
|
||||||
|
F: FnOnce(&Config) -> anyhow::Result<()>,
|
||||||
|
{
|
||||||
|
let previous_theme = self.cfg.theme;
|
||||||
|
self.cfg.theme = match self.cfg.theme {
|
||||||
|
Theme::Dark => Theme::Light,
|
||||||
|
Theme::Light => Theme::Dark,
|
||||||
|
};
|
||||||
|
|
||||||
|
match save(&self.cfg) {
|
||||||
|
Ok(()) => self.set_operator_note(format!(
|
||||||
|
"theme set to {} | saved to {}",
|
||||||
|
self.theme_label(),
|
||||||
|
config_path.display()
|
||||||
|
)),
|
||||||
|
Err(error) => {
|
||||||
|
self.cfg.theme = previous_theme;
|
||||||
|
self.set_operator_note(format!("failed to persist theme: {error}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn increase_pane_size(&mut self) {
|
pub fn increase_pane_size(&mut self) {
|
||||||
self.pane_size_percent =
|
self.pane_size_percent =
|
||||||
(self.pane_size_percent + PANE_RESIZE_STEP_PERCENT).min(MAX_PANE_SIZE_PERCENT);
|
(self.pane_size_percent + PANE_RESIZE_STEP_PERCENT).min(MAX_PANE_SIZE_PERCENT);
|
||||||
@@ -2371,7 +2411,7 @@ impl Dashboard {
|
|||||||
|
|
||||||
fn pane_border_style(&self, pane: Pane) -> Style {
|
fn pane_border_style(&self, pane: Pane) -> Style {
|
||||||
if self.selected_pane == pane {
|
if self.selected_pane == pane {
|
||||||
Style::default().fg(Color::Cyan)
|
Style::default().fg(self.theme_palette().accent)
|
||||||
} else {
|
} else {
|
||||||
Style::default()
|
Style::default()
|
||||||
}
|
}
|
||||||
@@ -2385,6 +2425,30 @@ impl Dashboard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn theme_label(&self) -> &'static str {
|
||||||
|
match self.cfg.theme {
|
||||||
|
Theme::Dark => "dark",
|
||||||
|
Theme::Light => "light",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn theme_palette(&self) -> ThemePalette {
|
||||||
|
match self.cfg.theme {
|
||||||
|
Theme::Dark => ThemePalette {
|
||||||
|
accent: Color::Cyan,
|
||||||
|
row_highlight_bg: Color::DarkGray,
|
||||||
|
muted: Color::DarkGray,
|
||||||
|
help_border: Color::Yellow,
|
||||||
|
},
|
||||||
|
Theme::Light => ThemePalette {
|
||||||
|
accent: Color::Blue,
|
||||||
|
row_highlight_bg: Color::Gray,
|
||||||
|
muted: Color::Black,
|
||||||
|
help_border: Color::Blue,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn log_field<'a>(&self, value: &'a str) -> &'a str {
|
fn log_field<'a>(&self, value: &'a str) -> &'a str {
|
||||||
let trimmed = value.trim();
|
let trimmed = value.trim();
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
@@ -4270,6 +4334,41 @@ diff --git a/src/next.rs b/src/next.rs
|
|||||||
let _ = std::fs::remove_dir_all(tempdir);
|
let _ = std::fs::remove_dir_all(tempdir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn toggle_theme_persists_config() {
|
||||||
|
let mut dashboard = test_dashboard(Vec::new(), 0);
|
||||||
|
let tempdir = std::env::temp_dir().join(format!("ecc2-theme-policy-{}", Uuid::new_v4()));
|
||||||
|
std::fs::create_dir_all(&tempdir).unwrap();
|
||||||
|
let config_path = tempdir.join("ecc2.toml");
|
||||||
|
|
||||||
|
dashboard.toggle_theme_with_save(&config_path, |cfg| cfg.save_to_path(&config_path));
|
||||||
|
|
||||||
|
assert_eq!(dashboard.cfg.theme, Theme::Light);
|
||||||
|
let expected_note = format!("theme set to light | saved to {}", config_path.display());
|
||||||
|
assert_eq!(
|
||||||
|
dashboard.operator_note.as_deref(),
|
||||||
|
Some(expected_note.as_str())
|
||||||
|
);
|
||||||
|
|
||||||
|
let saved = std::fs::read_to_string(&config_path).unwrap();
|
||||||
|
let loaded: Config = toml::from_str(&saved).unwrap();
|
||||||
|
assert_eq!(loaded.theme, Theme::Light);
|
||||||
|
let _ = std::fs::remove_dir_all(tempdir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn light_theme_uses_light_palette_accent() {
|
||||||
|
let mut dashboard = test_dashboard(Vec::new(), 0);
|
||||||
|
dashboard.cfg.theme = Theme::Light;
|
||||||
|
dashboard.selected_pane = Pane::Sessions;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
dashboard.pane_border_style(Pane::Sessions),
|
||||||
|
Style::default().fg(Color::Blue)
|
||||||
|
);
|
||||||
|
assert_eq!(dashboard.theme_palette().row_highlight_bg, Color::Gray);
|
||||||
|
}
|
||||||
|
|
||||||
fn test_dashboard(sessions: Vec<Session>, selected_session: usize) -> Dashboard {
|
fn test_dashboard(sessions: Vec<Session>, selected_session: usize) -> Dashboard {
|
||||||
let selected_session = selected_session.min(sessions.len().saturating_sub(1));
|
let selected_session = selected_session.min(sessions.len().saturating_sub(1));
|
||||||
let cfg = Config::default();
|
let cfg = Config::default();
|
||||||
|
|||||||
Reference in New Issue
Block a user