gglib_core/paths/
models.rs

1//! Models directory resolution.
2//!
3//! Provides utilities for resolving the models directory from explicit paths,
4//! environment variables, or platform defaults.
5
6use std::env;
7use std::path::PathBuf;
8
9use super::error::PathError;
10use super::platform::normalize_user_path;
11
12/// Default relative location for downloaded models on non-Windows platforms.
13#[cfg(not(target_os = "windows"))]
14pub const DEFAULT_MODELS_DIR_RELATIVE: &str = ".local/share/llama_models";
15
16/// How the models directory was derived.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ModelsDirSource {
19    /// The user passed an explicit path (e.g., CLI flag or GUI form).
20    Explicit,
21    /// The path came from environment variables / `.env`.
22    EnvVar,
23    /// Fallback default (`~/.local/share/llama_models` on Linux/macOS,
24    /// `%LOCALAPPDATA%\llama_models` on Windows).
25    Default,
26}
27
28/// Resolution result for the models directory.
29#[derive(Debug, Clone)]
30pub struct ModelsDirResolution {
31    /// The resolved path to the models directory.
32    pub path: PathBuf,
33    /// How the path was determined.
34    pub source: ModelsDirSource,
35}
36
37/// Return the platform-specific default models directory.
38///
39/// - **Windows**: `%LOCALAPPDATA%\llama_models` (e.g. `C:\Users\name\AppData\Local\llama_models`)
40/// - **macOS / Linux**: `~/.local/share/llama_models`
41pub fn default_models_dir() -> Result<PathBuf, PathError> {
42    #[cfg(target_os = "windows")]
43    {
44        let local_app_data = dirs::data_local_dir().ok_or(PathError::NoDataDir)?;
45        Ok(local_app_data.join("llama_models"))
46    }
47    #[cfg(not(target_os = "windows"))]
48    {
49        let home = dirs::home_dir().ok_or(PathError::NoHomeDir)?;
50        Ok(home.join(DEFAULT_MODELS_DIR_RELATIVE))
51    }
52}
53
54/// Resolve the models directory from an explicit override, env var, or default.
55///
56/// Resolution order:
57/// 1. Explicit path provided by caller (highest priority)
58/// 2. `GGLIB_MODELS_DIR` environment variable
59/// 3. Default models directory (`~/.local/share/llama_models`)
60pub fn resolve_models_dir(explicit: Option<&str>) -> Result<ModelsDirResolution, PathError> {
61    if let Some(path_str) = explicit {
62        return Ok(ModelsDirResolution {
63            path: normalize_user_path(path_str)?,
64            source: ModelsDirSource::Explicit,
65        });
66    }
67
68    if let Ok(env_path) = env::var("GGLIB_MODELS_DIR")
69        && !env_path.trim().is_empty()
70    {
71        return Ok(ModelsDirResolution {
72            path: normalize_user_path(&env_path)?,
73            source: ModelsDirSource::EnvVar,
74        });
75    }
76
77    Ok(ModelsDirResolution {
78        path: default_models_dir()?,
79        source: ModelsDirSource::Default,
80    })
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use crate::paths::test_utils::{ENV_LOCK, EnvVarGuard};
87
88    #[test]
89    fn test_default_models_dir_platform_path() {
90        let dir = default_models_dir().unwrap();
91        let path_str = dir.to_string_lossy();
92        // On Windows the path should be under %LOCALAPPDATA% and use native
93        // separators throughout — no forward-slash fragments.
94        #[cfg(target_os = "windows")]
95        {
96            assert!(
97                path_str.contains("llama_models"),
98                "Expected 'llama_models' in path: {path_str}"
99            );
100            assert!(
101                !path_str.contains('/'),
102                "Path must not contain forward slashes on Windows: {path_str}"
103            );
104        }
105        // On non-Windows the path should sit under ~/.local/share/llama_models.
106        #[cfg(not(target_os = "windows"))]
107        assert!(
108            path_str.contains(DEFAULT_MODELS_DIR_RELATIVE),
109            "Expected '{DEFAULT_MODELS_DIR_RELATIVE}' in path: {path_str}"
110        );
111    }
112
113    #[test]
114    fn test_resolve_models_dir_prefers_explicit() {
115        let _guard = ENV_LOCK.lock().unwrap();
116        let _env = EnvVarGuard::set("GGLIB_MODELS_DIR", "/tmp/env-value");
117        let resolved = resolve_models_dir(Some("/tmp/explicit")).unwrap();
118        assert_eq!(resolved.source, ModelsDirSource::Explicit);
119        assert!(resolved.path.ends_with("explicit"));
120    }
121
122    #[test]
123    fn test_resolve_models_dir_env_value() {
124        let _guard = ENV_LOCK.lock().unwrap();
125        let _env = EnvVarGuard::set("GGLIB_MODELS_DIR", "/tmp/from-env");
126        let resolved = resolve_models_dir(None).unwrap();
127        assert_eq!(resolved.source, ModelsDirSource::EnvVar);
128        assert!(resolved.path.ends_with("from-env"));
129    }
130}