cfgmatic_paths/lib.rs
1//! Cross-platform configuration path discovery following XDG and platform conventions.
2//!
3//! This crate provides a platform-agnostic way to discover configuration
4//! directories following the conventions of each operating system:
5//!
6//! - **Unix/Linux**: XDG Base Directory Specification
7//! - **macOS CLI**: XDG (same as Unix)
8//! - **macOS GUI**: Application Support directories
9//! - **Windows**: Known Folder IDs (`AppData`, `ProgramData`)
10//!
11//! # Quick Start
12//!
13//! ```
14//! use cfgmatic_paths::PathsBuilder;
15//!
16//! // Create a path finder for your application
17//! let finder = PathsBuilder::new("myapp").build();
18//!
19//! // Get user config directories
20//! let user_dirs = finder.user_dirs();
21//! println!("User config dirs: {:?}", user_dirs);
22//!
23//! // Ensure the primary user config directory exists
24//! if let Ok(config_dir) = finder.ensure_user_config_dir() {
25//! println!("Config dir: {}", config_dir.display());
26//! }
27//! ```
28//!
29//! # Configuration Tiers
30//!
31//! Configuration directories are organized into three tiers:
32//!
33//! 1. **User** (`ConfigTier::User`): User-specific configs in home directory.
34//! Highest priority, typically writable.
35//!
36//! 2. **Local** (`ConfigTier::Local`): Machine-specific configs.
37//! Medium priority, may be writable.
38//!
39//! 3. **System** (`ConfigTier::System`): System-wide configs.
40//! Lowest priority, typically read-only.
41//!
42//! # Platform Details
43//!
44//! ## Unix/Linux (XDG)
45//!
46//! Uses the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html):
47//!
48//! - User: `$XDG_CONFIG_HOME/<app>/` (default: `~/.config/<app>/`)
49//! - Legacy: `~/.<app>rc` (optional, enabled by default)
50//! - System: `$XDG_CONFIG_DIRS/<app>/` (default: `/etc/xdg/<app>/`)
51//!
52//! ## macOS
53//!
54//! ### CLI Applications
55//! Uses XDG (same as Unix/Linux).
56//!
57//! ### GUI Applications (with `macos-gui` feature)
58//! Uses macOS conventions:
59//!
60//! - User: `~/Library/Application Support/<bundle-id>/`
61//! - System: `/Library/Application Support/<bundle-id>/`
62//!
63//! ## Windows
64//!
65//! Uses Windows Known Folder IDs:
66//!
67//! - User Roaming: `%APPDATA%\<Company>\<App>\`
68//! - User Local: `%LOCALAPPDATA%\<Company>\<App>\`
69//! - System: `%PROGRAMDATA%\<Company>\<App>\`
70//!
71//! # Testing
72//!
73//! The crate provides abstractions for testing:
74//!
75//! - [`Env`]: Mock environment variables
76//! - [`Fs`]: Mock filesystem operations
77//!
78//! These allow testing without modifying the actual environment or filesystem.
79
80#![warn(missing_docs)]
81#![warn(clippy::missing_docs_in_private_items)]
82
83pub use builder::{PathFinder, PathsBuilder};
84pub use core::{
85 AppType, ConfigCandidate, ConfigDiscovery, ConfigFileRule, ConfigRuleSet, ConfigTier,
86 DiscoveryOptions, FilePattern, FragmentRule, PathStatus, RuleBasedDiscovery, RuleMatchResult,
87 SourceType, TierIterator, TierSearchMode,
88};
89pub use env::{Env, StdEnv};
90pub use filesystem::{Fs, StdFs};
91pub use platform::{DirectoryFinder, DirectoryInfo};
92
93mod builder;
94mod core;
95mod env;
96mod filesystem;
97mod platform;
98
99/// Platform-specific directory finder implementations.
100pub mod finders {
101
102 cfg_if::cfg_if! {
103 if #[cfg(all(target_os = "macos", feature = "macos-gui"))] {
104 pub use crate::platform::MacOSGuiDirectoryFinder;
105 } else if #[cfg(windows)] {
106 pub use crate::platform::WindowsDirectoryFinder;
107 } else {
108 pub use crate::platform::UnixDirectoryFinder;
109 }
110 }
111}
112
113/// Find the first existing configuration directory.
114///
115/// Searches in order: User → Local → System.
116/// Returns the first directory that exists, or `None`.
117///
118/// # Example
119///
120/// ```
121/// use cfgmatic_paths::find_existing_dir;
122///
123/// if let Some(dir) = find_existing_dir("myapp") {
124/// println!("Found config dir: {}", dir.display());
125/// }
126/// ```
127pub fn find_existing_dir(app_name: impl AsRef<str>) -> Option<std::path::PathBuf> {
128 PathsBuilder::new(app_name.as_ref())
129 .build()
130 .config_directories()
131 .into_iter()
132 .find(|dir| dir.exists())
133}
134
135/// Find or create the user configuration directory.
136///
137/// Returns the primary user config directory, creating it if it doesn't exist.
138///
139/// # Errors
140///
141/// Returns an error if the directory cannot be created.
142///
143/// # Example
144///
145/// ```
146/// use cfgmatic_paths::ensure_config_dir;
147///
148/// match ensure_config_dir("myapp") {
149/// Ok(dir) => println!("Config dir: {}", dir.display()),
150/// Err(e) => eprintln!("Failed to create config dir: {}", e),
151/// }
152/// ```
153pub fn ensure_config_dir(app_name: impl AsRef<str>) -> std::io::Result<std::path::PathBuf> {
154 PathsBuilder::new(app_name.as_ref())
155 .build()
156 .ensure_user_config_dir()
157}
158
159/// Get all configuration directories with their metadata.
160///
161/// Returns a list of all known configuration directories with their
162/// tier and existence status.
163///
164/// # Example
165///
166/// ```
167/// use cfgmatic_paths::get_all_dirs;
168///
169/// for info in get_all_dirs("myapp") {
170/// println!("{:?}: {} (exists: {})",
171/// info.tier, info.path.display(), info.exists);
172/// }
173/// ```
174pub fn get_all_dirs(app_name: impl AsRef<str>) -> Vec<DirectoryInfo> {
175 PathsBuilder::new(app_name.as_ref()).build().all_dirs()
176}
177
178/// Discover configuration with full diagnostics.
179///
180/// This is a convenience function that creates a finder and returns
181/// comprehensive information about configuration locations.
182///
183/// # Example
184///
185/// ```
186/// use cfgmatic_paths::discover_config;
187///
188/// let discovery = discover_config("myapp");
189/// println!("Preferred: {}", discovery.preferred_path.display());
190///
191/// for candidate in discovery.candidates {
192/// println!(" - {:?}: {} ({:?})",
193/// candidate.tier,
194/// candidate.path.display(),
195/// candidate.status
196/// );
197/// }
198/// ```
199pub fn discover_config(app_name: impl AsRef<str>) -> ConfigDiscovery {
200 PathsBuilder::new(app_name.as_ref())
201 .build()
202 .discover_config()
203}
204
205/// Get the preferred configuration path (without checking existence).
206///
207/// Returns the first user config directory path, regardless of whether
208/// it exists. This is useful for determining where to create a new
209/// configuration file.
210///
211/// # Example
212///
213/// ```
214/// use cfgmatic_paths::config_path;
215///
216/// let path = config_path("myapp");
217/// println!("Config would be at: {}", path.display());
218/// ```
219pub fn config_path(app_name: impl AsRef<str>) -> std::path::PathBuf {
220 PathsBuilder::new(app_name.as_ref())
221 .build()
222 .preferred_config_path()
223}
224
225/// Get the preferred configuration file path.
226///
227/// Returns the full path to a config file in the preferred location,
228/// without checking existence.
229///
230/// # Example
231///
232/// ```
233/// use cfgmatic_paths::config_file_path;
234///
235/// let path = config_file_path("myapp", "config.toml");
236/// println!("Config file would be at: {}", path.display());
237/// ```
238pub fn config_file_path(
239 app_name: impl AsRef<str>,
240 filename: impl AsRef<std::path::Path>,
241) -> std::path::PathBuf {
242 PathsBuilder::new(app_name.as_ref())
243 .build()
244 .preferred_config_file(filename)
245}