bkmr 6.4.1

A Unified CLI Tool for Bookmark, Snippet, and Knowledge Management
Documentation
// bkmr/src/app_state.rs
use crate::domain::embedding::Embedder;
use crate::infrastructure::embeddings::dummy_provider::DummyEmbedding;
use std::fmt;
use std::path::Path;
use std::sync::Arc;
use tracing::debug;

// Import our new config module
use crate::config::{load_settings, Settings};

/* Global AppState ("Service Locator" Style)
Global State Management:
The AppState struct and APP_STATE static variable provide global access to application state using
a thread-safe RwLock. This pattern is used throughout the codebase.

Service Locator Pattern:
The Context struct holds the embedder service which is used throughout the application.
This follows the service locator pattern.

Settings Storage:
The current implementation holds application settings and provides methods to update them.

API Compatibility:
Many parts of the codebase call methods like AppState::read_global() to access configuration and services.

need to keep AppState struct, but modify it to use our new configuration system internally.

  read-only access:
  let app_state = AppState::read_global();
  let db_url = &app_state.settings.db_url;
  let embedder = &app_state.context.embedder;

  This returns a read guard, preventing concurrent writes while it's in use.

  write access:
  let mut new_state = AppState::new();
  new_state.settings.db_url = "/my/new/path.db".to_string();
  AppState::update_global(new_state)?;

  refresh from environment:
  AppState::reload_settings()?;
*/
// Keep the Context struct as-is
pub struct Context {
    pub embedder: Arc<dyn Embedder>,
}

impl fmt::Debug for Context {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Context")
            .field("embedder", &"Arc<dyn Embedder>")
            .finish()
    }
}

impl Context {
    pub fn new(embedder: Arc<dyn Embedder>) -> Self {
        Self { embedder }
    }
}

#[derive(Debug)]
pub struct AppState {
    // holds infrastructure-like services
    pub context: Context,
    // holds configuration settings
    pub settings: Settings,
}

impl Default for AppState {
    fn default() -> Self {
        Self::new(Arc::new(DummyEmbedding))
    }
}

impl AppState {
    pub fn new(embedder: Arc<dyn Embedder>) -> Self {
        Self::new_with_config_file(embedder, None)
    }

    pub fn new_with_config_file(embedder: Arc<dyn Embedder>, config_file: Option<&Path>) -> Self {
        // Load settings using the new configuration system with optional config file
        let settings = load_settings(config_file).unwrap_or_else(|e| {
            debug!("Failed to load settings: {}. Using defaults.", e);
            Settings::default()
        });

        Self {
            context: Context::new(embedder),
            settings,
        }
    }
}

// Global state methods removed - using dependency injection instead

// Implement tests for backward compatibility
#[cfg(test)]
mod tests {
    use super::*;
    use crate::util::testing::{init_test_env, EnvGuard};
    use std::env;

    /// Helper for environment variable cleanup
    // struct EnvGuard {
    //     db_url: Option<String>,
    //     fzf_opts: Option<String>,
    // }
    //
    // impl EnvGuard {
    //     fn new() -> Self {
    //         Self {
    //             db_url: env::var("BKMR_DB_URL").ok(),
    //             fzf_opts: env::var("BKMR_FZF_OPTS").ok(),
    //         }
    //     }
    // }
    //
    // impl Drop for EnvGuard {
    //     fn drop(&mut self) {
    //         env::remove_var("BKMR_DB_URL");
    //         env::remove_var("BKMR_FZF_OPTS");
    //         if let Some(val) = &self.db_url {
    //             env::set_var("BKMR_DB_URL", val);
    //         }
    //         if let Some(val) = &self.fzf_opts {
    //             env::set_var("BKMR_FZF_OPTS", val);
    //         }
    //     }
    // }

    #[test]
    fn given_no_environment_when_new_then_defaults() {
        let _ = init_test_env();
        let _guard = EnvGuard::new();
        env::remove_var("BKMR_DB_URL");
        env::remove_var("BKMR_FZF_OPTS");

        let state = AppState::default();
        // Test that default values are used
        assert!(state.settings.db_url.contains("bkmr.db"));
        assert_eq!(state.settings.fzf_opts.height, "50%");
        assert!(!state.settings.fzf_opts.reverse);
        assert!(!state.settings.fzf_opts.show_tags);
    }

    #[test]
    fn given_env_vars_when_new_then_overrides() {
        let _ = init_test_env();
        let _guard = EnvGuard::new();
        env::set_var("BKMR_DB_URL", "/test/db.db");
        env::set_var("BKMR_FZF_OPTS", "--height 99% --reverse --show-tags");

        let state = AppState::default();
        assert_eq!(state.settings.db_url, "/test/db.db");
        assert_eq!(state.settings.fzf_opts.height, "99%");
        assert!(state.settings.fzf_opts.reverse);
        assert!(state.settings.fzf_opts.show_tags);
    }

    // Tests for global state methods - disabled after removing global state pattern
    // #[test]
    // fn given_global_state_when_update_then_state_changes() {
    //     let _guard = EnvGuard::new();
    //     let mut state = AppState::default();
    //     state.settings.db_url = "/some/db/path.db".to_string();
    //     AppState::update_global(state).unwrap();
    //
    //     let global = AppState::read_global();
    //     assert_eq!(global.settings.db_url, "/some/db/path.db");
    // }
    //
    // #[test]
    // fn given_modified_env_when_reload_then_settings_change() {
    //     let _guard = EnvGuard::new();
    //
    //     let mut initial = AppState::default();
    //     initial.settings.db_url = "/before-reload.db".to_string();
    //     AppState::update_global(initial).unwrap();
    //
    //     env::set_var("BKMR_DB_URL", "/after-reload.db");
    //     AppState::reload_settings().unwrap();
    //
    //     let reloaded = AppState::read_global();
    //     assert_eq!(reloaded.settings.db_url, "/after-reload.db");
    // }
}