@@ -5,20 +5,55 @@ use std::{
55
66use serde:: { Deserialize , Serialize } ;
77
8- /// On-disk workspace settings for a project folder.
9- /// Stored at `<project>/.stitchworkspace/workspace.json`.
8+ /* ============================ Workspace settings ============================ */
9+
1010#[ derive( Debug , Clone , Serialize , Deserialize , Default ) ]
1111pub struct WorkspaceSettings {
12- pub version : u32 , // for future migrations; currently 1
12+ pub version : u32 ,
1313 pub ext_filter : String ,
1414 pub exclude_dirs : String ,
1515 pub exclude_files : String ,
1616 pub remove_prefix : String ,
1717 pub remove_regex : String ,
1818 pub hierarchy_only : bool ,
1919 pub dirs_only : bool ,
20+
21+ /// Optional name of the currently-selected profile (if any).
22+ #[ serde( default ) ]
23+ pub current_profile : Option < String > ,
24+ }
25+
26+ /* ================================ Profiles ================================= */
27+
28+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
29+ pub enum ProfileScope {
30+ Shared ,
31+ Local ,
32+ }
33+
34+ #[ derive( Debug , Clone , Serialize , Deserialize , Default ) ]
35+ pub struct ProfileSelection {
36+ /// Project-relative path using forward slashes.
37+ pub path : String ,
38+ pub state : bool ,
39+ }
40+
41+ #[ derive( Debug , Clone , Serialize , Deserialize , Default ) ]
42+ pub struct Profile {
43+ pub name : String ,
44+ pub settings : WorkspaceSettings ,
45+ /// Explicit on/off checks captured relative to project root.
46+ pub explicit : Vec < ProfileSelection > ,
2047}
2148
49+ #[ derive( Debug , Clone ) ]
50+ pub struct ProfileMeta {
51+ pub name : String ,
52+ pub scope : ProfileScope ,
53+ }
54+
55+ /* ========================= Paths & basic workspace ========================= */
56+
2257pub fn workspace_dir ( project_root : & Path ) -> PathBuf {
2358 project_root. join ( ".stitchworkspace" )
2459}
@@ -27,7 +62,6 @@ pub fn workspace_file(project_root: &Path) -> PathBuf {
2762 workspace_dir ( project_root) . join ( "workspace.json" )
2863}
2964
30- /// Ensure `.stitchworkspace/` exists; return its path.
3165pub fn ensure_workspace_dir ( project_root : & Path ) -> io:: Result < PathBuf > {
3266 let dir = workspace_dir ( project_root) ;
3367 if !dir. exists ( ) {
@@ -36,14 +70,55 @@ pub fn ensure_workspace_dir(project_root: &Path) -> io::Result<PathBuf> {
3670 Ok ( dir)
3771}
3872
39- /// Try to load settings; returns `None` if file is missing or invalid.
73+ /* ============================ Profiles locations ============================ */
74+
75+ fn profiles_shared_dir ( project_root : & Path ) -> PathBuf {
76+ workspace_dir ( project_root) . join ( "profiles" )
77+ }
78+
79+ fn profiles_local_dir ( project_root : & Path ) -> PathBuf {
80+ workspace_dir ( project_root) . join ( "local" ) . join ( "profiles" )
81+ }
82+
83+ pub fn ensure_profiles_dirs ( project_root : & Path ) -> io:: Result < ( ) > {
84+ fs:: create_dir_all ( profiles_shared_dir ( project_root) ) ?;
85+ fs:: create_dir_all ( profiles_local_dir ( project_root) ) ?;
86+ Ok ( ( ) )
87+ }
88+
89+ fn sanitize_profile_name ( name : & str ) -> String {
90+ // keep it simple & predictable for file names
91+ let mut s = name. trim ( ) . to_string ( ) ;
92+ if s. is_empty ( ) {
93+ s. push_str ( "unnamed" ) ;
94+ }
95+ s. chars ( )
96+ . map ( |c| {
97+ if c. is_ascii_alphanumeric ( ) || c == '-' || c == '_' || c == ' ' {
98+ c
99+ } else {
100+ '_'
101+ }
102+ } )
103+ . collect ( )
104+ }
105+
106+ fn profile_path ( project_root : & Path , scope : ProfileScope , name : & str ) -> PathBuf {
107+ let base = match scope {
108+ ProfileScope :: Shared => profiles_shared_dir ( project_root) ,
109+ ProfileScope :: Local => profiles_local_dir ( project_root) ,
110+ } ;
111+ base. join ( format ! ( "{}.json" , sanitize_profile_name( name) ) )
112+ }
113+
114+ /* =============================== Workspace IO ============================== */
115+
40116pub fn load_workspace ( project_root : & Path ) -> Option < WorkspaceSettings > {
41117 let path = workspace_file ( project_root) ;
42118 let data = fs:: read ( & path) . ok ( ) ?;
43119 serde_json:: from_slice :: < WorkspaceSettings > ( & data) . ok ( )
44120}
45121
46- /// Save settings atomically to `workspace.json`.
47122pub fn save_workspace ( project_root : & Path , settings : & WorkspaceSettings ) -> io:: Result < ( ) > {
48123 ensure_workspace_dir ( project_root) ?;
49124
@@ -53,7 +128,82 @@ pub fn save_workspace(project_root: &Path, settings: &WorkspaceSettings) -> io::
53128 let data = serde_json:: to_vec_pretty ( settings) . map_err ( |e| io:: Error :: other ( e. to_string ( ) ) ) ?;
54129
55130 fs:: write ( & tmp, data) ?;
56- // Atomic on most platforms when same directory
57131 fs:: rename ( & tmp, & path) ?;
58132 Ok ( ( ) )
59133}
134+
135+ /* =============================== Profiles IO =============================== */
136+
137+ pub fn save_profile (
138+ project_root : & Path ,
139+ profile : & Profile ,
140+ scope : ProfileScope ,
141+ ) -> io:: Result < ( ) > {
142+ ensure_profiles_dirs ( project_root) ?;
143+ let path = profile_path ( project_root, scope, & profile. name ) ;
144+ let tmp = path. with_extension ( "tmp" ) ;
145+ let data = serde_json:: to_vec_pretty ( profile) . map_err ( |e| io:: Error :: other ( e. to_string ( ) ) ) ?;
146+ fs:: write ( & tmp, data) ?;
147+ fs:: rename ( & tmp, & path) ?;
148+ Ok ( ( ) )
149+ }
150+
151+ /// Returns (Profile, Scope) preferring Local if both exist.
152+ pub fn load_profile ( project_root : & Path , name : & str ) -> Option < ( Profile , ProfileScope ) > {
153+ let local = profile_path ( project_root, ProfileScope :: Local , name) ;
154+ if let Ok ( bytes) = fs:: read ( & local) {
155+ if let Ok ( p) = serde_json:: from_slice :: < Profile > ( & bytes) {
156+ return Some ( ( p, ProfileScope :: Local ) ) ;
157+ }
158+ }
159+ let shared = profile_path ( project_root, ProfileScope :: Shared , name) ;
160+ if let Ok ( bytes) = fs:: read ( & shared) {
161+ if let Ok ( p) = serde_json:: from_slice :: < Profile > ( & bytes) {
162+ return Some ( ( p, ProfileScope :: Shared ) ) ;
163+ }
164+ }
165+ None
166+ }
167+
168+ /// Lists all profiles found. If a name exists in both scopes, only the Local one is returned.
169+ pub fn list_profiles ( project_root : & Path ) -> Vec < ProfileMeta > {
170+ fn scan ( dir : & Path , scope : ProfileScope , out : & mut Vec < ( String , ProfileScope ) > ) {
171+ if let Ok ( rd) = fs:: read_dir ( dir) {
172+ for ent in rd. flatten ( ) {
173+ if let Some ( ext) = ent. path ( ) . extension ( ) {
174+ if ext == "json" {
175+ if let Some ( os) = ent. path ( ) . file_stem ( ) {
176+ let name = os. to_string_lossy ( ) . to_string ( ) ;
177+ out. push ( ( name, scope) ) ;
178+ }
179+ }
180+ }
181+ }
182+ }
183+ }
184+
185+ let mut raw: Vec < ( String , ProfileScope ) > = Vec :: new ( ) ;
186+ scan ( & profiles_shared_dir ( project_root) , ProfileScope :: Shared , & mut raw) ;
187+ scan ( & profiles_local_dir ( project_root) , ProfileScope :: Local , & mut raw) ;
188+
189+ // keep a single entry per name, preferring Local
190+ use std:: collections:: BTreeMap ;
191+ let mut by_name: BTreeMap < String , ProfileScope > = BTreeMap :: new ( ) ;
192+ for ( n, s) in raw {
193+ match by_name. get ( & n) {
194+ None => {
195+ by_name. insert ( n, s) ;
196+ }
197+ Some ( prev) => {
198+ if * prev == ProfileScope :: Shared && s == ProfileScope :: Local {
199+ by_name. insert ( n, s) ;
200+ }
201+ }
202+ }
203+ }
204+
205+ by_name
206+ . into_iter ( )
207+ . map ( |( name, scope) | ProfileMeta { name, scope } )
208+ . collect ( )
209+ }
0 commit comments