feat: integrate Google semantic search and branded desktop launcher
Core CI / core (ubuntu-latest) (push) Failing after 3m27s
Core CI / core (macos-latest) (push) Canceled after 0s
Core CI / core (windows-latest) (push) Canceled after 0s
Core CI / desktop (ubuntu-latest) (push) Successful in 5m45s
Core CI / desktop (macos-latest) (push) Canceled after 0s
Core CI / desktop (windows-latest) (push) Canceled after 0s

This commit is contained in:
karl committed 2026-10-12 17:44:56 +02:00
1 parent a90df15503
commit caf6a2f82e
80 files changed
+14971 -837

No files matched your search

+201
View File
@@ -0,0 +1,201 @@
//! Versioned local settings. Writes replace the destination atomically; failed
//! saves leave both the previous file and in-memory settings intact.
use serde::{Deserialize, Serialize};
use std::{
fs,
io::Write,
path::{Path, PathBuf},
};
const SETTINGS_VERSION: u32 = 1;
const MAX_SETTINGS_BYTES: u64 = 128 * 1024;
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct Settings {
pub version: u32,
pub roots: Vec<PathBuf>,
pub cpu_threads: usize,
/// Managed local Google EmbeddingGemma2 artifacts. Older settings default
/// to unconfigured; model acquisition is an explicit desktop action.
#[serde(default)]
pub model_directory: Option<PathBuf>,
}
impl Default for Settings {
fn default() -> Self {
Self {
version: SETTINGS_VERSION,
roots: Vec::new(),
cpu_threads: 1,
model_directory: None,
}
}
}
impl Settings {
pub fn validate(&self) -> Result<(), String> {
if self.version != SETTINGS_VERSION {
return Err(format!("Unsupported settings version {}", self.version));
}
if !(1..=8).contains(&self.cpu_threads) {
return Err("CPU threads must be between 1 and 8.".into());
}
if self.roots.len() > 64 {
return Err("At most 64 indexed folders are supported.".into());
}
if let Some(directory) = &self.model_directory {
if !directory.is_absolute() || directory.to_string_lossy().len() > 32768 {
return Err("Model directory must be an absolute bounded path.".into());
}
}
for (i, root) in self.roots.iter().enumerate() {
if !root.is_absolute() {
return Err("Indexed folder paths must be absolute.".into());
}
if root.to_string_lossy().len() > 32768 {
return Err("Indexed folder path is too long.".into());
}
if self.roots[..i].contains(root) {
return Err("Indexed folder paths must be unique.".into());
}
}
Ok(())
}
}
pub struct SettingsStore {
path: PathBuf,
pub value: Settings,
}
impl SettingsStore {
pub fn load(path: PathBuf) -> Result<Self, String> {
let value = match fs::metadata(&path) {
Ok(metadata) => {
if metadata.len() > MAX_SETTINGS_BYTES {
return Err("Settings file exceeds the size limit.".into());
}
serde_json::from_slice(&fs::read(&path).map_err(|e| e.to_string())?)
.map_err(|e| format!("Invalid settings file: {e}"))?
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Settings::default(),
Err(error) => return Err(error.to_string()),
};
let store = Self { path, value };
store.value.validate()?;
Ok(store)
}
pub fn save(&mut self, next: Settings) -> Result<(), String> {
next.validate()?;
let parent = self.path.parent().ok_or("Settings path has no parent.")?;
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
let mut temporary = tempfile::NamedTempFile::new_in(parent).map_err(|e| e.to_string())?;
let bytes = serde_json::to_vec_pretty(&next).map_err(|e| e.to_string())?;
if bytes.len() as u64 > MAX_SETTINGS_BYTES {
return Err("Settings exceed the size limit.".into());
}
temporary.write_all(&bytes).map_err(|e| e.to_string())?;
temporary.as_file().sync_all().map_err(|e| e.to_string())?;
temporary.persist(&self.path).map_err(|e| e.to_string())?;
self.value = next;
Ok(())
}
pub fn add_root(&mut self, root: &Path) -> Result<(), String> {
let canonical = fs::canonicalize(root).map_err(|e| e.to_string())?;
if !canonical.is_dir() {
return Err("Indexed folder must be a directory.".into());
}
let mut next = self.value.clone();
if !next.roots.contains(&canonical) {
next.roots.push(canonical);
self.save(next)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn settings_round_trip_and_atomic_replacement() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("settings.json");
let mut store = SettingsStore::load(path.clone()).unwrap();
assert_eq!(store.value.cpu_threads, 1);
store.add_root(directory.path()).unwrap();
store.add_root(directory.path()).unwrap();
let mut next = store.value.clone();
next.cpu_threads = 2;
store.save(next.clone()).unwrap();
assert_eq!(SettingsStore::load(path).unwrap().value, next);
assert_eq!(store.value.roots.len(), 1);
}
#[test]
fn invalid_save_preserves_previous_file_and_memory() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("settings.json");
let mut store = SettingsStore::load(path.clone()).unwrap();
store.save(Settings::default()).unwrap();
let original = fs::read(&path).unwrap();
let mut invalid = store.value.clone();
invalid.cpu_threads = 0;
assert!(store.save(invalid).is_err());
assert_eq!(store.value, Settings::default());
assert_eq!(fs::read(&path).unwrap(), original);
let mut invalid = store.value.clone();
invalid.roots.push(PathBuf::from("relative"));
assert!(store.save(invalid).is_err());
}
#[test]
fn rejects_corruption_unknown_version_and_excessive_threads() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("settings.json");
fs::write(&path, "broken").unwrap();
assert!(SettingsStore::load(path.clone()).is_err());
fs::write(&path, r#"{"version":99,"roots":[],"cpu_threads":1}"#).unwrap();
assert!(SettingsStore::load(path).is_err());
let invalid = Settings {
cpu_threads: 9,
..Settings::default()
};
assert!(invalid.validate().is_err());
}
#[test]
fn loads_older_settings_with_unconfigured_google_model() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("settings.json");
fs::write(&path, r#"{"version":1,"roots":[],"cpu_threads":1}"#).unwrap();
let store = SettingsStore::load(path).unwrap();
assert!(store.value.model_directory.is_none());
let invalid = Settings {
model_directory: Some(PathBuf::from("relative")),
..Settings::default()
};
assert!(invalid.validate().is_err());
}
#[test]
fn oversized_save_leaves_existing_settings_readable() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("settings.json");
let mut store = SettingsStore::load(path.clone()).unwrap();
store.save(Settings::default()).unwrap();
let mut oversized = store.value.clone();
oversized.roots = (0..64)
.map(|i| directory.path().join(format!("{i}{}", "x".repeat(3000))))
.collect();
assert!(store.save(oversized).is_err());
assert_eq!(
SettingsStore::load(path).unwrap().value,
Settings::default()
);
}
}