Add App core struct with event-handling and initialization logic for TUI.
This commit is contained in:
342
crates/owlen-core/src/config.rs
Normal file
342
crates/owlen-core/src/config.rs
Normal file
@@ -0,0 +1,342 @@
|
||||
use crate::provider::ProviderConfig;
|
||||
use crate::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Default location for the OWLEN configuration file
|
||||
pub const DEFAULT_CONFIG_PATH: &str = "~/.config/owlen/config.toml";
|
||||
|
||||
/// Core configuration shared by all OWLEN clients
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
/// General application settings
|
||||
pub general: GeneralSettings,
|
||||
/// Provider specific configuration keyed by provider name
|
||||
#[serde(default)]
|
||||
pub providers: HashMap<String, ProviderConfig>,
|
||||
/// UI preferences that frontends can opt into
|
||||
#[serde(default)]
|
||||
pub ui: UiSettings,
|
||||
/// Storage related options
|
||||
#[serde(default)]
|
||||
pub storage: StorageSettings,
|
||||
/// Input handling preferences
|
||||
#[serde(default)]
|
||||
pub input: InputSettings,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
let mut providers = HashMap::new();
|
||||
providers.insert(
|
||||
"ollama".to_string(),
|
||||
ProviderConfig {
|
||||
provider_type: "ollama".to_string(),
|
||||
base_url: Some("http://localhost:11434".to_string()),
|
||||
api_key: None,
|
||||
extra: HashMap::new(),
|
||||
},
|
||||
);
|
||||
|
||||
Self {
|
||||
general: GeneralSettings::default(),
|
||||
providers,
|
||||
ui: UiSettings::default(),
|
||||
storage: StorageSettings::default(),
|
||||
input: InputSettings::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Load configuration from disk, falling back to defaults when missing
|
||||
pub fn load(path: Option<&Path>) -> Result<Self> {
|
||||
let path = match path {
|
||||
Some(path) => path.to_path_buf(),
|
||||
None => default_config_path(),
|
||||
};
|
||||
|
||||
if path.exists() {
|
||||
let content = fs::read_to_string(&path)?;
|
||||
let mut config: Config =
|
||||
toml::from_str(&content).map_err(|e| crate::Error::Config(e.to_string()))?;
|
||||
config.ensure_defaults();
|
||||
Ok(config)
|
||||
} else {
|
||||
Ok(Config::default())
|
||||
}
|
||||
}
|
||||
|
||||
/// Persist configuration to disk
|
||||
pub fn save(&self, path: Option<&Path>) -> Result<()> {
|
||||
let path = match path {
|
||||
Some(path) => path.to_path_buf(),
|
||||
None => default_config_path(),
|
||||
};
|
||||
|
||||
if let Some(dir) = path.parent() {
|
||||
fs::create_dir_all(dir)?;
|
||||
}
|
||||
|
||||
let content =
|
||||
toml::to_string_pretty(self).map_err(|e| crate::Error::Config(e.to_string()))?;
|
||||
fs::write(path, content)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get provider configuration by provider name
|
||||
pub fn provider(&self, name: &str) -> Option<&ProviderConfig> {
|
||||
self.providers.get(name)
|
||||
}
|
||||
|
||||
/// Update or insert a provider configuration
|
||||
pub fn upsert_provider(&mut self, name: impl Into<String>, config: ProviderConfig) {
|
||||
self.providers.insert(name.into(), config);
|
||||
}
|
||||
|
||||
/// Resolve default model in order of priority: explicit default, first cached model, provider fallback
|
||||
pub fn resolve_default_model<'a>(
|
||||
&'a self,
|
||||
models: &'a [crate::types::ModelInfo],
|
||||
) -> Option<&'a str> {
|
||||
if let Some(model) = self.general.default_model.as_deref() {
|
||||
if models.iter().any(|m| m.id == model || m.name == model) {
|
||||
return Some(model);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(first) = models.first() {
|
||||
return Some(&first.id);
|
||||
}
|
||||
|
||||
self.general.default_model.as_deref()
|
||||
}
|
||||
|
||||
fn ensure_defaults(&mut self) {
|
||||
if self.general.default_provider.is_empty() {
|
||||
self.general.default_provider = "ollama".to_string();
|
||||
}
|
||||
|
||||
if !self.providers.contains_key("ollama") {
|
||||
self.providers.insert(
|
||||
"ollama".to_string(),
|
||||
ProviderConfig {
|
||||
provider_type: "ollama".to_string(),
|
||||
base_url: Some("http://localhost:11434".to_string()),
|
||||
api_key: None,
|
||||
extra: HashMap::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default configuration path with user home expansion
|
||||
pub fn default_config_path() -> PathBuf {
|
||||
PathBuf::from(shellexpand::tilde(DEFAULT_CONFIG_PATH).as_ref())
|
||||
}
|
||||
|
||||
/// General behaviour settings shared across clients
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GeneralSettings {
|
||||
/// Default provider name for routing
|
||||
pub default_provider: String,
|
||||
/// Optional default model id
|
||||
#[serde(default)]
|
||||
pub default_model: Option<String>,
|
||||
/// Whether streaming responses are preferred
|
||||
#[serde(default = "GeneralSettings::default_streaming")]
|
||||
pub enable_streaming: bool,
|
||||
/// Optional path to a project context file automatically injected as system prompt
|
||||
#[serde(default)]
|
||||
pub project_context_file: Option<String>,
|
||||
/// TTL for cached model listings in seconds
|
||||
#[serde(default = "GeneralSettings::default_model_cache_ttl")]
|
||||
pub model_cache_ttl_secs: u64,
|
||||
}
|
||||
|
||||
impl GeneralSettings {
|
||||
fn default_streaming() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_model_cache_ttl() -> u64 {
|
||||
60
|
||||
}
|
||||
|
||||
/// Duration representation of model cache TTL
|
||||
pub fn model_cache_ttl(&self) -> Duration {
|
||||
Duration::from_secs(self.model_cache_ttl_secs.max(5))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GeneralSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
default_provider: "ollama".to_string(),
|
||||
default_model: Some("llama3.2:latest".to_string()),
|
||||
enable_streaming: Self::default_streaming(),
|
||||
project_context_file: Some("OWLEN.md".to_string()),
|
||||
model_cache_ttl_secs: Self::default_model_cache_ttl(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// UI preferences that consumers can respect as needed
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UiSettings {
|
||||
#[serde(default = "UiSettings::default_theme")]
|
||||
pub theme: String,
|
||||
#[serde(default = "UiSettings::default_word_wrap")]
|
||||
pub word_wrap: bool,
|
||||
#[serde(default = "UiSettings::default_max_history_lines")]
|
||||
pub max_history_lines: usize,
|
||||
#[serde(default = "UiSettings::default_show_role_labels")]
|
||||
pub show_role_labels: bool,
|
||||
#[serde(default = "UiSettings::default_wrap_column")]
|
||||
pub wrap_column: u16,
|
||||
}
|
||||
|
||||
impl UiSettings {
|
||||
fn default_theme() -> String {
|
||||
"default".to_string()
|
||||
}
|
||||
|
||||
fn default_word_wrap() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_max_history_lines() -> usize {
|
||||
2000
|
||||
}
|
||||
|
||||
fn default_show_role_labels() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_wrap_column() -> u16 {
|
||||
100
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UiSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
theme: Self::default_theme(),
|
||||
word_wrap: Self::default_word_wrap(),
|
||||
max_history_lines: Self::default_max_history_lines(),
|
||||
show_role_labels: Self::default_show_role_labels(),
|
||||
wrap_column: Self::default_wrap_column(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Storage related preferences
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StorageSettings {
|
||||
#[serde(default = "StorageSettings::default_conversation_dir")]
|
||||
pub conversation_dir: String,
|
||||
#[serde(default = "StorageSettings::default_auto_save")]
|
||||
pub auto_save_sessions: bool,
|
||||
#[serde(default = "StorageSettings::default_max_sessions")]
|
||||
pub max_saved_sessions: usize,
|
||||
#[serde(default = "StorageSettings::default_session_timeout")]
|
||||
pub session_timeout_minutes: u64,
|
||||
}
|
||||
|
||||
impl StorageSettings {
|
||||
fn default_conversation_dir() -> String {
|
||||
"~/.local/share/owlen/conversations".to_string()
|
||||
}
|
||||
|
||||
fn default_auto_save() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_max_sessions() -> usize {
|
||||
25
|
||||
}
|
||||
|
||||
fn default_session_timeout() -> u64 {
|
||||
120
|
||||
}
|
||||
|
||||
/// Resolve storage directory path
|
||||
pub fn conversation_path(&self) -> PathBuf {
|
||||
PathBuf::from(shellexpand::tilde(&self.conversation_dir).as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for StorageSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
conversation_dir: Self::default_conversation_dir(),
|
||||
auto_save_sessions: Self::default_auto_save(),
|
||||
max_saved_sessions: Self::default_max_sessions(),
|
||||
session_timeout_minutes: Self::default_session_timeout(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Input handling preferences shared across clients
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InputSettings {
|
||||
#[serde(default = "InputSettings::default_multiline")]
|
||||
pub multiline: bool,
|
||||
#[serde(default = "InputSettings::default_history_size")]
|
||||
pub history_size: usize,
|
||||
#[serde(default = "InputSettings::default_tab_width")]
|
||||
pub tab_width: u8,
|
||||
#[serde(default = "InputSettings::default_confirm_send")]
|
||||
pub confirm_send: bool,
|
||||
}
|
||||
|
||||
impl InputSettings {
|
||||
fn default_multiline() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_history_size() -> usize {
|
||||
100
|
||||
}
|
||||
|
||||
fn default_tab_width() -> u8 {
|
||||
4
|
||||
}
|
||||
|
||||
fn default_confirm_send() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InputSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
multiline: Self::default_multiline(),
|
||||
history_size: Self::default_history_size(),
|
||||
tab_width: Self::default_tab_width(),
|
||||
confirm_send: Self::default_confirm_send(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience accessor for an Ollama provider entry, creating a default if missing
|
||||
pub fn ensure_ollama_config(config: &mut Config) -> &ProviderConfig {
|
||||
config
|
||||
.providers
|
||||
.entry("ollama".to_string())
|
||||
.or_insert_with(|| ProviderConfig {
|
||||
provider_type: "ollama".to_string(),
|
||||
base_url: Some("http://localhost:11434".to_string()),
|
||||
api_key: None,
|
||||
extra: HashMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Calculate absolute timeout for session data based on configuration
|
||||
pub fn session_timeout(config: &Config) -> Duration {
|
||||
Duration::from_secs(config.storage.session_timeout_minutes.max(1) * 60)
|
||||
}
|
||||
Reference in New Issue
Block a user