lean_ctx/cli/
theme_cmd.rs1use crate::core::config;
2use crate::core::theme;
3
4pub fn cmd_theme(args: &[String]) {
5 let sub = args.first().map_or("list", std::string::String::as_str);
6 let r = theme::rst();
7 let b = theme::bold();
8 let d = theme::dim();
9
10 match sub {
11 "list" => {
12 let cfg = config::Config::load();
13 let active = cfg.theme.as_str();
14 println!();
15 println!(" {b}Available themes:{r}");
16 println!(" {ln}", ln = "─".repeat(40));
17 for name in theme::PRESET_NAMES {
18 let marker = if *name == active { " ◀ active" } else { "" };
19 let Some(t) = theme::from_preset(name) else {
20 continue;
21 };
22 let preview = format!(
23 "{p}██{r}{s}██{r}{a}██{r}{sc}██{r}{w}██{r}",
24 p = t.primary.fg(),
25 s = t.secondary.fg(),
26 a = t.accent.fg(),
27 sc = t.success.fg(),
28 w = t.warning.fg(),
29 );
30 println!(" {preview} {b}{name:<12}{r}{d}{marker}{r}");
31 }
32 if let Some(path) = theme::theme_file_path()
33 && path.exists()
34 {
35 let custom = theme::load_theme("_custom_");
36 let preview = format!(
37 "{p}██{r}{s}██{r}{a}██{r}{sc}██{r}{w}██{r}",
38 p = custom.primary.fg(),
39 s = custom.secondary.fg(),
40 a = custom.accent.fg(),
41 sc = custom.success.fg(),
42 w = custom.warning.fg(),
43 );
44 let marker = if active == "custom" {
45 " ◀ active"
46 } else {
47 ""
48 };
49 println!(" {preview} {b}{:<12}{r}{d}{marker}{r}", custom.name);
50 }
51 println!();
52 println!(" {d}Set theme: lean-ctx theme set <name>{r}");
53 println!();
54 }
55 "set" => {
56 if args.len() < 2 {
57 eprintln!("Usage: lean-ctx theme set <name>");
58 std::process::exit(1);
59 }
60 let name = &args[1];
61 if theme::from_preset(name).is_none() && name != "custom" {
62 eprintln!(
63 "Unknown theme '{name}'. Available: {}",
64 theme::PRESET_NAMES.join(", ")
65 );
66 std::process::exit(1);
67 }
68 match config::Config::update_global(|cfg| cfg.theme.clone_from(name)) {
69 Ok(_) => {
70 let t = theme::load_theme(name);
71 println!(" {sc}✓{r} Theme set to {b}{name}{r}", sc = t.success.fg());
72 let preview = t.gradient_bar(0.75, 30);
73 println!(" {preview}");
74 }
75 Err(e) => eprintln!("Error: {e}"),
76 }
77 }
78 "export" => {
79 let cfg = config::Config::load();
80 let t = theme::load_theme(&cfg.theme);
81 println!("{}", t.to_toml());
82 }
83 "import" => {
84 if args.len() < 2 {
85 eprintln!("Usage: lean-ctx theme import <path>");
86 std::process::exit(1);
87 }
88 let path = std::path::Path::new(&args[1]);
89 if !path.exists() {
90 eprintln!("File not found: {}", args[1]);
91 std::process::exit(1);
92 }
93 match std::fs::read_to_string(path) {
94 Ok(content) => match toml::from_str::<theme::Theme>(&content) {
95 Ok(imported) => match theme::save_theme(&imported) {
96 Ok(()) => {
97 if let Err(e) = config::Config::update_global(|cfg| {
98 cfg.theme = "custom".to_string();
99 }) {
100 tracing::warn!("could not persist theme: {e}");
101 }
102 println!(
103 " {sc}✓{r} Imported theme '{name}' → ~/.lean-ctx/theme.toml",
104 sc = imported.success.fg(),
105 name = imported.name,
106 );
107 println!(" Config updated: theme = custom");
108 }
109 Err(e) => eprintln!("Error saving theme: {e}"),
110 },
111 Err(e) => eprintln!("Invalid theme file: {e}"),
112 },
113 Err(e) => eprintln!("Error reading file: {e}"),
114 }
115 }
116 "preview" => {
117 let name = args.get(1).map_or("default", std::string::String::as_str);
118 let Some(t) = theme::from_preset(name) else {
119 eprintln!("Unknown theme: {name}");
120 std::process::exit(1);
121 };
122 println!();
123 println!(
124 " {icon} {title} {d}Theme Preview: {name}{r}",
125 icon = t.header_icon(),
126 title = t.brand_title(),
127 );
128 println!(" {ln}", ln = t.border_line(50));
129 println!();
130 println!(
131 " {b}{sc} 1.2M {r} {b}{sec} 87.3% {r} {b}{wrn} 4,521 {r} {b}{acc} $12.50 {r}",
132 sc = t.success.fg(),
133 sec = t.secondary.fg(),
134 wrn = t.warning.fg(),
135 acc = t.accent.fg(),
136 );
137 println!(" {d} tokens saved compression commands USD saved{r}");
138 println!();
139 println!(
140 " {b}{txt}Gradient Bar{r} {bar}",
141 txt = t.text.fg(),
142 bar = t.gradient_bar(0.85, 30),
143 );
144 println!(
145 " {b}{txt}Sparkline{r} {spark}",
146 txt = t.text.fg(),
147 spark = t.gradient_sparkline(&[20, 40, 30, 80, 60, 90, 70]),
148 );
149 println!();
150 println!(" {top}", top = t.box_top(50));
151 println!(
152 " {side} {b}{txt}Box content with themed borders{r} {side_r}",
153 side = t.box_side(),
154 side_r = t.box_side(),
155 txt = t.text.fg(),
156 );
157 println!(" {bot}", bot = t.box_bottom(50));
158 println!();
159 }
160 "tokens" => {
161 let format = args.get(1).map_or("css", String::as_str);
162 let cfg = config::Config::load();
163 let t = theme::load_theme(&cfg.theme);
164 match format {
165 "css" => println!("{}", t.to_css_vars()),
166 "js" => print!("{}", t.to_js_tokens()),
167 _ => {
168 eprintln!("Usage: lean-ctx theme tokens [css|js]");
169 std::process::exit(1);
170 }
171 }
172 }
173 _ => {
174 eprintln!("Usage: lean-ctx theme [list|set|export|import|preview|tokens]");
175 std::process::exit(1);
176 }
177 }
178}