11use clap:: { Parser , ValueEnum } ;
2+ use serde:: { Deserialize , Serialize } ;
23use std:: path:: PathBuf ;
34
45#[ derive( Parser ) ]
@@ -28,6 +29,12 @@ pub struct Cli {
2829 #[ arg( short = 't' , long, value_name = "LEVEL" , default_value = "latest" ) ]
2930 pub target : TargetLevel ,
3031
32+ /// Preview all color schemes, or set one persistently.
33+ /// Without a value: show all schemes visually.
34+ /// With a value: save that scheme and exit.
35+ #[ arg( long, value_name = "SCHEME" , num_args = 0 ..=1 , default_missing_value = "" ) ]
36+ pub set_color_scheme : Option < String > ,
37+
3138 /// Update pycu itself to the latest release
3239 #[ arg( long) ]
3340 pub self_update : bool ,
@@ -49,9 +56,49 @@ pub enum TargetLevel {
4956 Patch ,
5057}
5158
59+ #[ derive( ValueEnum , Serialize , Deserialize , Clone , PartialEq , Debug ) ]
60+ #[ serde( rename_all = "kebab-case" ) ]
61+ pub enum ColorScheme {
62+ /// #D73A49 / #0366D6 / #28A745 - GitHub-style SemVer severity (default)
63+ Default ,
64+ /// #E69F00 / #0072B2 / #009E73 - Okabe–Ito, color-blind safe
65+ OkabeIto ,
66+ /// #E74C3C / #F1C40F / #2ECC71 - traffic-light (red/yellow/green)
67+ TrafficLight ,
68+ /// #8E44AD / #3498DB / #95A5A6 - monitoring style (purple/blue/gray)
69+ Severity ,
70+ /// #CC79A7 / #0072B2 / #F0E442 - maximum distinction, color-blind safe
71+ HighContrast ,
72+ }
73+
5274pub async fn run ( ) -> anyhow:: Result < ( ) > {
5375 let cli = Cli :: parse ( ) ;
5476
77+ if let Some ( raw) = cli. set_color_scheme {
78+ if raw. is_empty ( ) {
79+ // --set-color-scheme used without a value: show the preview
80+ crate :: output:: table:: print_color_scheme_preview ( ) ;
81+ } else {
82+ // --set-color-scheme <SCHEME>: parse, save, confirm
83+ use clap:: ValueEnum ;
84+ let scheme = ColorScheme :: from_str ( & raw , true ) . map_err ( |e| anyhow:: anyhow!(
85+ "Unknown color scheme '{}'. {}\n Run `pycu --set-color-scheme` to see all options." ,
86+ raw, e
87+ ) ) ?;
88+ let config = crate :: config:: Config {
89+ color_scheme : scheme. clone ( ) ,
90+ } ;
91+ crate :: config:: save ( & config) ?;
92+ let path = crate :: config:: config_path ( ) . unwrap_or_else ( || PathBuf :: from ( "config.toml" ) ) ;
93+ println ! (
94+ "Color scheme set to '{}' and saved to {}." ,
95+ raw,
96+ path. display( )
97+ ) ;
98+ }
99+ return Ok ( ( ) ) ;
100+ }
101+
55102 // Self-update is independent of any project file
56103 if cli. self_update {
57104 let client = crate :: pypi:: client:: PypiClient :: new ( ) ?. into_inner ( ) ;
@@ -62,6 +109,12 @@ pub async fn run() -> anyhow::Result<()> {
62109 return crate :: uninstall:: run ( ) ;
63110 }
64111
112+ // Load persisted config, running first-run setup if no config exists or is unreadable
113+ let config = match crate :: config:: load ( ) {
114+ Ok ( Some ( cfg) ) => cfg,
115+ Ok ( None ) | Err ( _) => crate :: config:: first_run_setup ( ) ?,
116+ } ;
117+
65118 let file_path = match cli. file {
66119 Some ( p) => p,
67120 None => resolve_default_file ( ) ?,
@@ -97,7 +150,7 @@ pub async fn run() -> anyhow::Result<()> {
97150 . collect ( ) ;
98151
99152 if cli. upgrade {
100- crate :: output:: table:: print_table ( & updates, false ) ;
153+ crate :: output:: table:: print_table ( & updates, false , & config . color_scheme ) ;
101154 let count = crate :: upgrade:: apply_upgrades ( & file_path, & updates) ?;
102155 if count > 0 {
103156 println ! (
@@ -113,7 +166,7 @@ pub async fn run() -> anyhow::Result<()> {
113166 if cli. json {
114167 crate :: output:: json:: print_json ( & updates) ?;
115168 } else {
116- crate :: output:: table:: print_table ( & updates, true ) ;
169+ crate :: output:: table:: print_table ( & updates, true , & config . color_scheme ) ;
117170 }
118171
119172 Ok ( ( ) )
0 commit comments