//! Yoi のホームディレクトリ配下のパス解決を一元化するモジュール。 //! //! 用途別に三つの base directory を持つ: //! //! - **`config_dir`** — 人が手で書く / 編集する設定。`profiles.toml`, //! `providers.toml`, `models.toml`, `prompts/`, `prompts.toml` 等 //! - **`data_dir`** — プログラムが書く永続データ。`sessions/` 等 //! - **`runtime_dir`** — 再起動で消えてよいランタイム状態。socket, //! `pods.json`, `pid` ファイル等 //! //! ## 解決順 (優先順位高 → 低) //! //! | base | 1. `YOI__DIR` | 2. `YOI_HOME` | 3. `XDG_*` | 4. 既定 | //! |---|---|---|---|---| //! | config | `YOI_CONFIG_DIR` | `$YOI_HOME/config` | `$XDG_CONFIG_HOME/yoi` | `$HOME/.config/yoi` | //! | data | `YOI_DATA_DIR` | `$YOI_HOME` | — | `$HOME/.yoi` | //! | runtime | `YOI_RUNTIME_DIR` | `$YOI_HOME/run` | `$XDG_RUNTIME_DIR/yoi` | `$HOME/.yoi/run` | //! //! `YOI_HOME=$X` のとき config は `$X/config`、data は `$X` 直下、 //! runtime は `$X/run` に集約される。テストや sandbox 利用ではこれ一本 //! で全部 tempdir に向けられる。 //! //! 解決された各 base が存在するか / ディレクトリかは保証しない — //! 呼び出し側がファイル操作の前に作成 / 検査する。 use std::path::PathBuf; /// 設定ディレクトリ。`profiles.toml`, `providers.toml`, `models.toml`, /// `prompts/` などが置かれる。 pub fn config_dir() -> Option { resolve_config_dir_from_parts( env_path("YOI_CONFIG_DIR"), env_path("YOI_HOME"), env_path("XDG_CONFIG_HOME"), env_path("HOME"), ) } /// データディレクトリ。`sessions/` などプログラムが書く永続データの /// 置き場。 pub fn data_dir() -> Option { resolve_data_dir_from_parts( env_path("YOI_DATA_DIR"), env_path("YOI_HOME"), env_path("HOME"), ) } /// ランタイムディレクトリ。socket, `pods.json`, Pod ごとの `pid` / /// `status.json` 等が置かれる。再起動で消えて構わない。 pub fn runtime_dir() -> Option { resolve_runtime_dir_from_parts( env_path("YOI_RUNTIME_DIR"), env_path("YOI_HOME"), env_path("XDG_RUNTIME_DIR"), env_path("HOME"), ) } // ---- well-known file getters ------------------------------------------------ /// `/profiles.toml` — user profile registry/default configuration. /// /// This is application/profile selection configuration, not a Pod manifest /// layer. pub fn user_profiles_path() -> Option { user_profiles_path_from_config_dir(config_dir()) } /// `/prompts/` — user prompts ライブラリ。 pub fn user_prompts_dir() -> Option { user_prompts_dir_from_config_dir(config_dir()) } /// `/prompts.toml` — user prompt pack。 pub fn user_pack_file() -> Option { user_pack_file_from_config_dir(config_dir()) } /// `/` — providers.toml / models.toml 等の /// user override ファイル。 pub fn user_catalog_override(file_name: &str) -> Option { user_catalog_override_from_config_dir(config_dir(), file_name) } /// `/sessions/` — session store のデフォルト位置。 pub fn sessions_dir() -> Option { sessions_dir_from_data_dir(data_dir()) } /// `/pods.json` — machine-wide Pod allocation registry。 pub fn pod_registry_path() -> Option { pod_registry_path_from_runtime_dir(runtime_dir()) } /// `//` — Pod ごとのランタイムディレクトリ。 pub fn pod_runtime_dir(pod_name: &str) -> Option { pod_runtime_dir_from_runtime_dir(runtime_dir(), pod_name) } /// `//sock` — Pod の Unix socket パス。 /// /// Pod プロセス内で実際に socket を作成するのは `pod` crate の /// `RuntimeDir::socket_path()` で、Pod 名が分かっている外部 (TUI の /// attach フロー等) からの**予測**はこの関数で行う。両者は同じパス /// を返すことが期待される。 pub fn pod_socket_path(pod_name: &str) -> Option { pod_socket_path_from_runtime_dir(runtime_dir(), pod_name) } // ---- internals -------------------------------------------------------------- fn resolve_config_dir_from_parts( yoi_config_dir: Option, yoi_home: Option, xdg_config_home: Option, home: Option, ) -> Option { if let Some(p) = yoi_config_dir { return Some(p); } if let Some(p) = yoi_home { return Some(p.join("config")); } if let Some(p) = xdg_config_home { return Some(p.join("yoi")); } Some(home?.join(".config").join("yoi")) } fn resolve_data_dir_from_parts( yoi_data_dir: Option, yoi_home: Option, home: Option, ) -> Option { if let Some(p) = yoi_data_dir { return Some(p); } if let Some(p) = yoi_home { return Some(p); } Some(home?.join(".yoi")) } fn resolve_runtime_dir_from_parts( yoi_runtime_dir: Option, yoi_home: Option, xdg_runtime_dir: Option, home: Option, ) -> Option { if let Some(p) = yoi_runtime_dir { return Some(p); } if let Some(p) = yoi_home { return Some(p.join("run")); } if let Some(p) = xdg_runtime_dir { return Some(p.join("yoi")); } Some(home?.join(".yoi").join("run")) } fn user_profiles_path_from_config_dir(config_dir: Option) -> Option { Some(config_dir?.join("profiles.toml")) } fn user_prompts_dir_from_config_dir(config_dir: Option) -> Option { Some(config_dir?.join("prompts")) } fn user_pack_file_from_config_dir(config_dir: Option) -> Option { Some(config_dir?.join("prompts.toml")) } fn user_catalog_override_from_config_dir( config_dir: Option, file_name: &str, ) -> Option { Some(config_dir?.join(file_name)) } fn sessions_dir_from_data_dir(data_dir: Option) -> Option { Some(data_dir?.join("sessions")) } fn pod_registry_path_from_runtime_dir(runtime_dir: Option) -> Option { Some(runtime_dir?.join("pods.json")) } fn pod_runtime_dir_from_runtime_dir( runtime_dir: Option, pod_name: &str, ) -> Option { Some(runtime_dir?.join(pod_name)) } fn pod_socket_path_from_runtime_dir( runtime_dir: Option, pod_name: &str, ) -> Option { Some(pod_runtime_dir_from_runtime_dir(runtime_dir, pod_name)?.join("sock")) } /// 空文字列の env は未設定として扱う。`std::env::var` は `Ok("")` と /// `Err(NotPresent)` を区別するが、パス解決においては両者を未設定と /// 同等に扱うのが直感的。 fn env_path(name: &str) -> Option { let value = std::env::var(name).ok()?; path_from_env_value(Some(value.as_str())) } fn path_from_env_value(value: Option<&str>) -> Option { value.filter(|s| !s.is_empty()).map(PathBuf::from) } #[cfg(test)] mod tests { use super::*; #[test] fn config_dir_falls_back_to_home_dot_config() { assert_eq!( resolve_config_dir_from_parts(None, None, None, Some(PathBuf::from("/h"))).unwrap(), PathBuf::from("/h/.config/yoi") ); } #[test] fn config_dir_uses_xdg_when_set() { assert_eq!( resolve_config_dir_from_parts( None, None, Some(PathBuf::from("/x")), Some(PathBuf::from("/h")), ) .unwrap(), PathBuf::from("/x/yoi") ); } #[test] fn config_dir_yoi_home_outranks_xdg() { assert_eq!( resolve_config_dir_from_parts( None, Some(PathBuf::from("/sand")), Some(PathBuf::from("/x")), Some(PathBuf::from("/h")), ) .unwrap(), PathBuf::from("/sand/config") ); } #[test] fn config_dir_explicit_wins_over_yoi_home() { assert_eq!( resolve_config_dir_from_parts( Some(PathBuf::from("/explicit-cfg")), Some(PathBuf::from("/sand")), None, Some(PathBuf::from("/h")), ) .unwrap(), PathBuf::from("/explicit-cfg") ); } #[test] fn data_dir_default_is_dot_yoi() { assert_eq!( resolve_data_dir_from_parts(None, None, Some(PathBuf::from("/h"))).unwrap(), PathBuf::from("/h/.yoi") ); } #[test] fn data_dir_yoi_home_is_data_dir_itself() { assert_eq!( resolve_data_dir_from_parts( None, Some(PathBuf::from("/sand")), Some(PathBuf::from("/h")) ) .unwrap(), PathBuf::from("/sand") ); } #[test] fn data_dir_explicit_wins_over_yoi_home() { assert_eq!( resolve_data_dir_from_parts( Some(PathBuf::from("/explicit-data")), Some(PathBuf::from("/sand")), Some(PathBuf::from("/h")), ) .unwrap(), PathBuf::from("/explicit-data") ); } #[test] fn runtime_dir_prefers_xdg_runtime_dir() { assert_eq!( resolve_runtime_dir_from_parts( None, None, Some(PathBuf::from("/xdg-runtime")), Some(PathBuf::from("/h")), ) .unwrap(), PathBuf::from("/xdg-runtime/yoi") ); } #[test] fn runtime_dir_falls_back_to_dot_yoi_run() { assert_eq!( resolve_runtime_dir_from_parts(None, None, None, Some(PathBuf::from("/h"))).unwrap(), PathBuf::from("/h/.yoi/run") ); } #[test] fn runtime_dir_yoi_home_is_run_subdir() { assert_eq!( resolve_runtime_dir_from_parts( None, Some(PathBuf::from("/sand")), Some(PathBuf::from("/run/user/1000")), Some(PathBuf::from("/h")), ) .unwrap(), PathBuf::from("/sand/run") ); } #[test] fn runtime_dir_explicit_wins_over_yoi_home() { assert_eq!( resolve_runtime_dir_from_parts( Some(PathBuf::from("/explicit-run")), Some(PathBuf::from("/sand")), Some(PathBuf::from("/run/user/1000")), Some(PathBuf::from("/h")), ) .unwrap(), PathBuf::from("/explicit-run") ); } #[test] fn empty_env_value_treated_as_unset_before_path_resolution() { let xdg_config_home = path_from_env_value(Some("")); assert_eq!( resolve_config_dir_from_parts(None, None, xdg_config_home, Some(PathBuf::from("/h"))) .unwrap(), PathBuf::from("/h/.config/yoi") ); } #[test] fn returns_none_when_nothing_set() { assert!(resolve_config_dir_from_parts(None, None, None, None).is_none()); assert!(resolve_data_dir_from_parts(None, None, None).is_none()); assert!(resolve_runtime_dir_from_parts(None, None, None, None).is_none()); } #[test] fn well_known_files_compose_off_base_dirs() { let config_dir = Some(PathBuf::from("/sand/config")); let data_dir = Some(PathBuf::from("/sand")); let runtime_dir = Some(PathBuf::from("/sand/run")); assert_eq!( user_profiles_path_from_config_dir(config_dir.clone()).unwrap(), PathBuf::from("/sand/config/profiles.toml") ); assert_eq!( user_prompts_dir_from_config_dir(config_dir.clone()).unwrap(), PathBuf::from("/sand/config/prompts") ); assert_eq!( user_pack_file_from_config_dir(config_dir.clone()).unwrap(), PathBuf::from("/sand/config/prompts.toml") ); assert_eq!( user_catalog_override_from_config_dir(config_dir, "providers.toml").unwrap(), PathBuf::from("/sand/config/providers.toml") ); assert_eq!( sessions_dir_from_data_dir(data_dir).unwrap(), PathBuf::from("/sand/sessions") ); assert_eq!( pod_registry_path_from_runtime_dir(runtime_dir.clone()).unwrap(), PathBuf::from("/sand/run/pods.json") ); assert_eq!( pod_runtime_dir_from_runtime_dir(runtime_dir.clone(), "foo").unwrap(), PathBuf::from("/sand/run/foo") ); assert_eq!( pod_socket_path_from_runtime_dir(runtime_dir, "foo").unwrap(), PathBuf::from("/sand/run/foo/sock") ); } }