@@ -7,7 +7,7 @@ use std::{
77
88use directories:: BaseDirs ;
99use owo_colors:: OwoColorize ;
10- use vite_path:: AbsolutePathBuf ;
10+ use vite_path:: { AbsolutePath , AbsolutePathBuf } ;
1111use vite_shared:: output;
1212use vite_str:: Str ;
1313
@@ -23,6 +23,14 @@ const SHELL_PROFILES: &[(&str, bool)] = &[
2323 ( ".config/fish/conf.d/vite-plus.fish" , true ) ,
2424] ;
2525
26+ /// Abbreviate a path for display: replace `$HOME` prefix with `~`.
27+ fn abbreviate_home_path ( path : & AbsolutePath , user_home : & AbsolutePath ) -> Str {
28+ match path. strip_prefix ( user_home) {
29+ Ok ( Some ( suffix) ) => vite_str:: format!( "~/{suffix}" ) ,
30+ _ => Str :: from ( path. to_string ( ) ) ,
31+ }
32+ }
33+
2634/// Comment marker written by the install script above the sourcing line.
2735const VITE_PLUS_COMMENT : & str = "# Vite+ bin" ;
2836
@@ -97,26 +105,52 @@ enum AffectedProfileKind {
97105/// Content is cached so we don't need to re-read during cleaning.
98106fn collect_affected_profiles ( user_home : & AbsolutePathBuf ) -> Vec < AffectedProfile > {
99107 let mut affected = Vec :: new ( ) ;
100- for & ( name, is_snippet) in SHELL_PROFILES {
101- let path = user_home. join ( name) ;
108+
109+ // Build full list of (display_name, path, is_snippet) from the base set
110+ let mut profiles: Vec < ( Str , AbsolutePathBuf , bool ) > = SHELL_PROFILES
111+ . iter ( )
112+ . map ( |& ( name, is_snippet) | {
113+ ( vite_str:: format!( "~/{name}" ) , user_home. join ( name) , is_snippet)
114+ } )
115+ . collect ( ) ;
116+
117+ // If ZDOTDIR is set and differs from $HOME, also check there.
118+ if let Ok ( zdotdir) = std:: env:: var ( "ZDOTDIR" )
119+ && let Some ( zdotdir_path) = AbsolutePathBuf :: new ( zdotdir. into ( ) )
120+ && zdotdir_path != * user_home
121+ {
122+ for name in [ ".zshenv" , ".zshrc" ] {
123+ let path = zdotdir_path. join ( name) ;
124+ let display = abbreviate_home_path ( & path, user_home) ;
125+ profiles. push ( ( display, path, false ) ) ;
126+ }
127+ }
128+
129+ // If XDG_CONFIG_HOME is set and differs from $HOME/.config, also check there.
130+ if let Ok ( xdg_config) = std:: env:: var ( "XDG_CONFIG_HOME" )
131+ && let Some ( xdg_path) = AbsolutePathBuf :: new ( xdg_config. into ( ) )
132+ && xdg_path != user_home. join ( ".config" )
133+ {
134+ let path = xdg_path. join ( "fish/conf.d/vite-plus.fish" ) ;
135+ let display = abbreviate_home_path ( & path, user_home) ;
136+ profiles. push ( ( display, path, true ) ) ;
137+ }
138+
139+ for ( name, path, is_snippet) in profiles {
102140 // For snippets, check if the file exists only
103141 if is_snippet {
104- if let Some ( true ) = std:: fs:: exists ( & path) . ok ( ) {
105- affected. push ( AffectedProfile {
106- name : Str :: from ( name) ,
107- path,
108- kind : AffectedProfileKind :: Snippet ,
109- } )
142+ if let Ok ( true ) = std:: fs:: exists ( & path) {
143+ affected. push ( AffectedProfile { name, path, kind : AffectedProfileKind :: Snippet } )
110144 }
111145 continue ;
112146 }
113147 // Read directly — if the file doesn't exist, read_to_string returns Err
114- // which is_ok_and handles gracefully (no redundant exists() check).
148+ // which .ok().filter() handles gracefully (no redundant exists() check).
115149 if let Some ( content) =
116150 std:: fs:: read_to_string ( & path) . ok ( ) . filter ( |c| has_vite_plus_lines ( c) )
117151 {
118152 affected. push ( AffectedProfile {
119- name : Str :: from ( name ) ,
153+ name,
120154 path,
121155 kind : AffectedProfileKind :: Main { content : Str :: from ( content) } ,
122156 } ) ;
@@ -144,7 +178,7 @@ fn confirm_implode(
144178 if !affected_profiles. is_empty ( ) {
145179 output:: raw ( " Shell profiles to clean:" ) ;
146180 for profile in affected_profiles {
147- output:: raw ( & vite_str:: format!( " - ~/ {}" , profile. name) ) ;
181+ output:: raw ( & vite_str:: format!( " - {}" , profile. name) ) ;
148182 }
149183 }
150184 output:: raw ( "" ) ;
@@ -171,16 +205,16 @@ fn clean_affected_profiles(affected_profiles: &[AffectedProfile]) {
171205 AffectedProfileKind :: Main { content } => {
172206 let cleaned = remove_vite_plus_lines ( content) ;
173207 match std:: fs:: write ( & profile. path , cleaned. as_bytes ( ) ) {
174- Ok ( ( ) ) => output:: success ( & vite_str:: format!( "Cleaned ~/ {}" , profile. name) ) ,
208+ Ok ( ( ) ) => output:: success ( & vite_str:: format!( "Cleaned {}" , profile. name) ) ,
175209 Err ( e) => {
176- output:: warn ( & vite_str:: format!( "Failed to clean ~/ {}: {e}" , profile. name) ) ;
210+ output:: warn ( & vite_str:: format!( "Failed to clean {}: {e}" , profile. name) ) ;
177211 }
178212 }
179213 }
180214 AffectedProfileKind :: Snippet => match std:: fs:: remove_file ( & profile. path ) {
181- Ok ( ( ) ) => output:: success ( & vite_str:: format!( "Removed ~/ {}" , profile. name) ) ,
215+ Ok ( ( ) ) => output:: success ( & vite_str:: format!( "Removed {}" , profile. name) ) ,
182216 Err ( e) => {
183- output:: warn ( & vite_str:: format!( "Failed to remove ~/ {}: {e}" , profile. name) ) ;
217+ output:: warn ( & vite_str:: format!( "Failed to remove {}: {e}" , profile. name) ) ;
184218 }
185219 } ,
186220 }
@@ -336,6 +370,9 @@ fn remove_windows_path_entry(bin_path: &vite_path::AbsolutePath) -> std::io::Res
336370
337371#[ cfg( test) ]
338372mod tests {
373+ #[ cfg( not( windows) ) ]
374+ use serial_test:: serial;
375+
339376 use super :: * ;
340377
341378 #[ test]
@@ -420,6 +457,130 @@ mod tests {
420457 assert ! ( script. contains( "timeout /T 1 /NOBREAK" ) ) ;
421458 }
422459
460+ #[ test]
461+ #[ cfg( not( windows) ) ]
462+ fn test_abbreviate_home_path ( ) {
463+ let home = AbsolutePathBuf :: new ( "/home/user" . into ( ) ) . unwrap ( ) ;
464+ // Under home → ~/...
465+ let under = AbsolutePathBuf :: new ( "/home/user/.zshrc" . into ( ) ) . unwrap ( ) ;
466+ assert_eq ! ( & * abbreviate_home_path( & under, & home) , "~/.zshrc" ) ;
467+ // Outside home → absolute path as-is
468+ let outside = AbsolutePathBuf :: new ( "/opt/zdotdir/.zshenv" . into ( ) ) . unwrap ( ) ;
469+ assert_eq ! ( & * abbreviate_home_path( & outside, & home) , "/opt/zdotdir/.zshenv" ) ;
470+ }
471+
472+ #[ test]
473+ #[ serial]
474+ #[ cfg( not( windows) ) ]
475+ fn test_collect_affected_profiles ( ) {
476+ let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
477+ let home = AbsolutePathBuf :: new ( temp_dir. path ( ) . to_path_buf ( ) ) . unwrap ( ) ;
478+
479+ // Clear ZDOTDIR/XDG_CONFIG_HOME so the test environment doesn't affect results
480+ let _guard = ProfileEnvGuard :: new ( None , None ) ;
481+
482+ // Main profile with vite-plus line
483+ std:: fs:: write ( home. join ( ".zshrc" ) , ". \" $HOME/.vite-plus/env\" \n " ) . unwrap ( ) ;
484+ // Unrelated profile (should be ignored)
485+ std:: fs:: write ( home. join ( ".bashrc" ) , "export PATH=/usr/bin\n " ) . unwrap ( ) ;
486+ // Snippet file (just needs to exist)
487+ let fish_dir = home. join ( ".config/fish/conf.d" ) ;
488+ std:: fs:: create_dir_all ( & fish_dir) . unwrap ( ) ;
489+ std:: fs:: write ( fish_dir. join ( "vite-plus.fish" ) , "source ~/.vite-plus/env.fish\n " ) . unwrap ( ) ;
490+
491+ let profiles = collect_affected_profiles ( & home) ;
492+ assert_eq ! ( profiles. len( ) , 2 ) ;
493+ assert ! ( matches!( & profiles[ 0 ] . kind, AffectedProfileKind :: Main { .. } ) ) ;
494+ assert ! ( matches!( & profiles[ 1 ] . kind, AffectedProfileKind :: Snippet ) ) ;
495+ }
496+
497+ /// Guard that saves and restores ZDOTDIR and XDG_CONFIG_HOME env vars.
498+ #[ cfg( not( windows) ) ]
499+ struct ProfileEnvGuard {
500+ original_zdotdir : Option < std:: ffi:: OsString > ,
501+ original_xdg_config : Option < std:: ffi:: OsString > ,
502+ }
503+
504+ #[ cfg( not( windows) ) ]
505+ impl ProfileEnvGuard {
506+ fn new ( zdotdir : Option < & std:: path:: Path > , xdg_config : Option < & std:: path:: Path > ) -> Self {
507+ let guard = Self {
508+ original_zdotdir : std:: env:: var_os ( "ZDOTDIR" ) ,
509+ original_xdg_config : std:: env:: var_os ( "XDG_CONFIG_HOME" ) ,
510+ } ;
511+ unsafe {
512+ match zdotdir {
513+ Some ( v) => std:: env:: set_var ( "ZDOTDIR" , v) ,
514+ None => std:: env:: remove_var ( "ZDOTDIR" ) ,
515+ }
516+ match xdg_config {
517+ Some ( v) => std:: env:: set_var ( "XDG_CONFIG_HOME" , v) ,
518+ None => std:: env:: remove_var ( "XDG_CONFIG_HOME" ) ,
519+ }
520+ }
521+ guard
522+ }
523+ }
524+
525+ #[ cfg( not( windows) ) ]
526+ impl Drop for ProfileEnvGuard {
527+ fn drop ( & mut self ) {
528+ unsafe {
529+ match & self . original_zdotdir {
530+ Some ( v) => std:: env:: set_var ( "ZDOTDIR" , v) ,
531+ None => std:: env:: remove_var ( "ZDOTDIR" ) ,
532+ }
533+ match & self . original_xdg_config {
534+ Some ( v) => std:: env:: set_var ( "XDG_CONFIG_HOME" , v) ,
535+ None => std:: env:: remove_var ( "XDG_CONFIG_HOME" ) ,
536+ }
537+ }
538+ }
539+ }
540+
541+ #[ test]
542+ #[ serial]
543+ #[ cfg( not( windows) ) ]
544+ fn test_collect_affected_profiles_zdotdir ( ) {
545+ let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
546+ let home = AbsolutePathBuf :: new ( temp_dir. path ( ) . join ( "home" ) ) . unwrap ( ) ;
547+ let zdotdir = temp_dir. path ( ) . join ( "zdotdir" ) ;
548+ std:: fs:: create_dir_all ( & home) . unwrap ( ) ;
549+ std:: fs:: create_dir_all ( & zdotdir) . unwrap ( ) ;
550+
551+ std:: fs:: write ( zdotdir. join ( ".zshenv" ) , ". \" $HOME/.vite-plus/env\" \n " ) . unwrap ( ) ;
552+
553+ let _guard = ProfileEnvGuard :: new ( Some ( & zdotdir) , None ) ;
554+
555+ let profiles = collect_affected_profiles ( & home) ;
556+ let zdotdir_profiles: Vec < _ > =
557+ profiles. iter ( ) . filter ( |p| p. path . as_path ( ) . starts_with ( & zdotdir) ) . collect ( ) ;
558+ assert_eq ! ( zdotdir_profiles. len( ) , 1 ) ;
559+ assert ! ( matches!( & zdotdir_profiles[ 0 ] . kind, AffectedProfileKind :: Main { .. } ) ) ;
560+ }
561+
562+ #[ test]
563+ #[ serial]
564+ #[ cfg( not( windows) ) ]
565+ fn test_collect_affected_profiles_xdg_config ( ) {
566+ let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
567+ let home = AbsolutePathBuf :: new ( temp_dir. path ( ) . join ( "home" ) ) . unwrap ( ) ;
568+ let xdg_config = temp_dir. path ( ) . join ( "xdg_config" ) ;
569+ let fish_dir = xdg_config. join ( "fish/conf.d" ) ;
570+ std:: fs:: create_dir_all ( & home) . unwrap ( ) ;
571+ std:: fs:: create_dir_all ( & fish_dir) . unwrap ( ) ;
572+
573+ std:: fs:: write ( fish_dir. join ( "vite-plus.fish" ) , "" ) . unwrap ( ) ;
574+
575+ let _guard = ProfileEnvGuard :: new ( None , Some ( & xdg_config) ) ;
576+
577+ let profiles = collect_affected_profiles ( & home) ;
578+ let xdg_profiles: Vec < _ > =
579+ profiles. iter ( ) . filter ( |p| p. path . as_path ( ) . starts_with ( & xdg_config) ) . collect ( ) ;
580+ assert_eq ! ( xdg_profiles. len( ) , 1 ) ;
581+ assert ! ( matches!( & xdg_profiles[ 0 ] . kind, AffectedProfileKind :: Snippet ) ) ;
582+ }
583+
423584 #[ test]
424585 fn test_execute_not_installed ( ) {
425586 let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
0 commit comments