11use std:: fs;
22use std:: io:: { self , Write } ;
3- use std:: path:: PathBuf ;
3+ use std:: path:: { Path , PathBuf } ;
44
55use anyhow:: { Context , Result } ;
66use serde:: { Deserialize , Serialize } ;
@@ -35,11 +35,7 @@ pub fn load() -> Result<Option<Config>> {
3535 return Ok ( None ) ;
3636 }
3737
38- let contents = fs:: read_to_string ( & path)
39- . with_context ( || format ! ( "Failed to read config file: {}" , path. display( ) ) ) ?;
40- let config: Config = toml:: from_str ( & contents)
41- . with_context ( || format ! ( "Failed to parse config file: {}" , path. display( ) ) ) ?;
42- Ok ( Some ( config) )
38+ load_from_path ( & path) . map ( Some )
4339}
4440
4541pub fn save ( config : & Config ) -> Result < ( ) > {
@@ -48,15 +44,7 @@ pub fn save(config: &Config) -> Result<()> {
4844 None => anyhow:: bail!( "Could not determine config directory" ) ,
4945 } ;
5046
51- if let Some ( parent) = path. parent ( ) {
52- fs:: create_dir_all ( parent)
53- . with_context ( || format ! ( "Failed to create config directory: {}" , parent. display( ) ) ) ?;
54- }
55-
56- let contents = toml:: to_string ( config) . context ( "Failed to serialize config" ) ?;
57- fs:: write ( & path, contents)
58- . with_context ( || format ! ( "Failed to write config file: {}" , path. display( ) ) ) ?;
59- Ok ( ( ) )
47+ save_to_path ( config, & path)
6048}
6149
6250/// Interactive first-run prompt. Shows the color scheme preview, asks the user to pick,
@@ -71,16 +59,8 @@ pub fn first_run_setup() -> Result<Config> {
7159
7260 let mut input = String :: new ( ) ;
7361 io:: stdin ( ) . read_line ( & mut input) ?;
74- let input = input. trim ( ) . to_lowercase ( ) ;
75-
76- let color_scheme = match input. as_str ( ) {
77- "okabe-ito" => ColorScheme :: OkabeIto ,
78- "traffic-light" => ColorScheme :: TrafficLight ,
79- "severity" => ColorScheme :: Severity ,
80- "high-contrast" => ColorScheme :: HighContrast ,
81- _ => ColorScheme :: Default ,
82- } ;
8362
63+ let color_scheme = parse_scheme_input ( & input) ;
8464 let config = Config { color_scheme } ;
8565 save ( & config) ?;
8666
@@ -92,6 +72,36 @@ pub fn first_run_setup() -> Result<Config> {
9272 Ok ( config)
9373}
9474
75+ // ── Internal helpers (pub(crate) for testing) ────────────────────────────────
76+
77+ pub ( crate ) fn load_from_path ( path : & Path ) -> Result < Config > {
78+ let contents = fs:: read_to_string ( path)
79+ . with_context ( || format ! ( "Failed to read config file: {}" , path. display( ) ) ) ?;
80+ toml:: from_str ( & contents)
81+ . with_context ( || format ! ( "Failed to parse config file: {}" , path. display( ) ) )
82+ }
83+
84+ pub ( crate ) fn save_to_path ( config : & Config , path : & Path ) -> Result < ( ) > {
85+ if let Some ( parent) = path. parent ( ) {
86+ fs:: create_dir_all ( parent)
87+ . with_context ( || format ! ( "Failed to create config directory: {}" , parent. display( ) ) ) ?;
88+ }
89+ let contents = toml:: to_string ( config) . context ( "Failed to serialize config" ) ?;
90+ fs:: write ( path, contents)
91+ . with_context ( || format ! ( "Failed to write config file: {}" , path. display( ) ) )
92+ }
93+
94+ /// Parse a raw user input string into a `ColorScheme`, defaulting to `Default`.
95+ pub ( crate ) fn parse_scheme_input ( input : & str ) -> ColorScheme {
96+ match input. trim ( ) . to_lowercase ( ) . as_str ( ) {
97+ "okabe-ito" => ColorScheme :: OkabeIto ,
98+ "traffic-light" => ColorScheme :: TrafficLight ,
99+ "severity" => ColorScheme :: Severity ,
100+ "high-contrast" => ColorScheme :: HighContrast ,
101+ _ => ColorScheme :: Default ,
102+ }
103+ }
104+
95105#[ cfg( test) ]
96106mod tests {
97107 use super :: * ;
@@ -131,23 +141,70 @@ mod tests {
131141 }
132142
133143 #[ test]
134- fn test_nonexistent_path_has_no_file ( ) {
135- let fake_path = PathBuf :: from ( "/nonexistent/pycu/config.toml" ) ;
136- assert ! ( !fake_path. exists( ) ) ;
144+ fn test_save_to_path_creates_dirs_and_file ( ) {
145+ let dir = TempDir :: new ( ) . unwrap ( ) ;
146+ let path = dir. path ( ) . join ( "nested" ) . join ( "pycu" ) . join ( "config.toml" ) ;
147+ let cfg = Config {
148+ color_scheme : ColorScheme :: Severity ,
149+ } ;
150+ save_to_path ( & cfg, & path) . unwrap ( ) ;
151+ assert ! ( path. exists( ) ) ;
137152 }
138153
139154 #[ test]
140- fn test_save_and_load_roundtrip ( ) {
155+ fn test_load_from_path_roundtrip ( ) {
141156 let dir = TempDir :: new ( ) . unwrap ( ) ;
142157 let path = dir. path ( ) . join ( "config.toml" ) ;
143-
144158 let cfg = Config {
145- color_scheme : ColorScheme :: OkabeIto ,
159+ color_scheme : ColorScheme :: TrafficLight ,
146160 } ;
147- let contents = toml:: to_string ( & cfg) . unwrap ( ) ;
148- std:: fs:: write ( & path, contents) . unwrap ( ) ;
161+ save_to_path ( & cfg, & path) . unwrap ( ) ;
162+ let back = load_from_path ( & path) . unwrap ( ) ;
163+ assert_eq ! ( back. color_scheme, ColorScheme :: TrafficLight ) ;
164+ }
149165
150- let back: Config = toml:: from_str ( & std:: fs:: read_to_string ( & path) . unwrap ( ) ) . unwrap ( ) ;
151- assert_eq ! ( back. color_scheme, ColorScheme :: OkabeIto ) ;
166+ #[ test]
167+ fn test_load_from_path_invalid_toml_returns_err ( ) {
168+ let dir = TempDir :: new ( ) . unwrap ( ) ;
169+ let path = dir. path ( ) . join ( "config.toml" ) ;
170+ fs:: write ( & path, "this is not valid toml !!!" ) . unwrap ( ) ;
171+ let result = load_from_path ( & path) ;
172+ assert ! ( result. is_err( ) ) ;
173+ assert ! ( result. unwrap_err( ) . to_string( ) . contains( "Failed to parse" ) ) ;
174+ }
175+
176+ #[ test]
177+ fn test_load_from_path_missing_file_returns_err ( ) {
178+ let dir = TempDir :: new ( ) . unwrap ( ) ;
179+ let path = dir. path ( ) . join ( "nonexistent.toml" ) ;
180+ let result = load_from_path ( & path) ;
181+ assert ! ( result. is_err( ) ) ;
182+ assert ! ( result. unwrap_err( ) . to_string( ) . contains( "Failed to read" ) ) ;
183+ }
184+
185+ #[ test]
186+ fn test_parse_scheme_input_all_variants ( ) {
187+ assert_eq ! ( parse_scheme_input( "okabe-ito" ) , ColorScheme :: OkabeIto ) ;
188+ assert_eq ! (
189+ parse_scheme_input( "traffic-light" ) ,
190+ ColorScheme :: TrafficLight
191+ ) ;
192+ assert_eq ! ( parse_scheme_input( "severity" ) , ColorScheme :: Severity ) ;
193+ assert_eq ! (
194+ parse_scheme_input( "high-contrast" ) ,
195+ ColorScheme :: HighContrast
196+ ) ;
197+ assert_eq ! ( parse_scheme_input( "default" ) , ColorScheme :: Default ) ;
198+ assert_eq ! ( parse_scheme_input( "" ) , ColorScheme :: Default ) ;
199+ assert_eq ! ( parse_scheme_input( "unknown" ) , ColorScheme :: Default ) ;
200+ }
201+
202+ #[ test]
203+ fn test_parse_scheme_input_trims_whitespace_and_ignores_case ( ) {
204+ assert_eq ! ( parse_scheme_input( " OKABE-ITO\n " ) , ColorScheme :: OkabeIto ) ;
205+ assert_eq ! (
206+ parse_scheme_input( " Traffic-Light " ) ,
207+ ColorScheme :: TrafficLight
208+ ) ;
152209 }
153210}
0 commit comments