use std::io::Write;
use std::path::PathBuf;
use std::sync::Arc;
use santui_core::sync::{SyncClient, SyncOp};
use santui_core::{DbAccess, Santui};
use santui_db::open_db;
use santui_registry::Registry;
#[cfg(feature = "auth")]
use santui_auth::AuthClient;
use rusqlite::Connection;
const VERSION: &str = env!("CARGO_PKG_VERSION");
struct SantuiDb {
conn: Option<Connection>,
sync: Option<Arc<SyncClient>>,
}
impl DbAccess for SantuiDb {
fn get_value(&self, plugin: &str, user_id: &str, key: &str) -> Option<String> {
let conn = self.conn.as_ref()?;
conn.query_row(
"SELECT value FROM user_data WHERE plugin = ?1 AND user_id = ?2 AND key = ?3",
rusqlite::params![plugin, user_id, key],
|row| row.get::<_, String>(0),
)
.ok()
}
fn set_value(&mut self, plugin: &str, user_id: &str, key: &str, value: &str) {
let conn = match self.conn.as_ref() {
Some(c) => c,
None => return,
};
let _ = conn.execute(
"INSERT INTO user_data (plugin, user_id, key, value) VALUES (?1, ?2, ?3, ?4)
ON CONFLICT(plugin, user_id, key) DO UPDATE SET value = excluded.value",
rusqlite::params![plugin, user_id, key, value],
);
if let Some(ref sync) = self.sync {
sync.enqueue(SyncOp::Set {
plugin: plugin.to_string(),
key: key.to_string(),
value: value.to_string(),
});
}
}
}
fn old_data_dir() -> PathBuf {
let home = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.unwrap_or_else(|_| ".".into());
PathBuf::from(home).join(".santui")
}
fn resolve_data_dir() -> PathBuf {
if std::env::var("SANTUI_DEV").as_deref() == Ok("1") {
old_data_dir()
} else {
santui_db::data_dir()
}
}
fn migrate_data_dir(old: &std::path::Path, new: &std::path::Path) {
let entries = match std::fs::read_dir(old) {
Ok(e) => e,
Err(_) => return,
};
for entry in entries.flatten() {
let name = entry.file_name();
let src = entry.path();
let dst = new.join(&name);
if src.is_dir() {
if let Err(e) = copy_dir_recursively(&src, &dst) {
log::warn!("failed to migrate directory {:?}: {e}", name);
}
} else if let Err(e) = std::fs::copy(&src, &dst) {
log::warn!("failed to migrate file {:?}: {e}", name);
}
}
}
fn copy_dir_recursively(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let file_type = entry.file_type()?;
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if file_type.is_dir() {
copy_dir_recursively(&src_path, &dst_path)?;
} else {
std::fs::copy(&src_path, &dst_path)?;
}
}
Ok(())
}
fn reset() -> Result<(), Box<dyn std::error::Error>> {
let data_dir = resolve_data_dir();
let db_dir = santui_db::data_dir();
println!("This will permanently delete all Santui data:");
println!(" Config, plugins, registry: {:?}", data_dir);
if db_dir != data_dir {
println!(" Database & auth tokens: {:?}", db_dir);
}
print!("Are you sure? (y/N): ");
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
if !(input.trim().eq_ignore_ascii_case("y") || input.trim().eq_ignore_ascii_case("yes")) {
println!("Reset cancelled.");
return Ok(());
}
let mut deleted = false;
if data_dir.exists() {
std::fs::remove_dir_all(&data_dir)?;
deleted = true;
}
if db_dir != data_dir && db_dir.exists() {
std::fs::remove_dir_all(&db_dir)?;
deleted = true;
}
if deleted {
println!("Santui data cleared. Run `santui` to start fresh.");
} else {
println!("No Santui data found.");
}
Ok(())
}
fn list_plugins() -> Result<(), Box<dyn std::error::Error>> {
let mut reg = Registry::new(resolve_data_dir());
println!("santui v{VERSION}");
println!();
println!("Installed plugins:");
if reg.installed.is_empty() {
println!(" (none)");
} else {
for p in ®.installed {
let status = if p.enabled { "enabled" } else { "disabled" };
println!(" {} v{} [{}]", p.name, p.version, status);
if !p.capabilities.is_empty() {
println!(" capabilities: {}", p.capabilities.join(", "));
}
}
}
println!();
match reg.fetch_manifest() {
Ok(()) => {
let installed_ids: std::collections::HashSet<&str> =
reg.installed.iter().map(|p| p.id.as_str()).collect();
let not_installed: Vec<&santui_registry::PluginManifest> = reg
.available
.iter()
.filter(|m| !installed_ids.contains(m.id.as_str()))
.collect();
println!("Available plugins ({} not installed):", not_installed.len());
if not_installed.is_empty() {
println!(" (all installed)");
} else {
for m in ¬_installed {
println!(" {} v{} — {}", m.name, m.version, m.description);
}
}
}
Err(e) => {
println!("Available plugins:");
println!(" (could not fetch manifest: {e})");
}
}
Ok(())
}
#[cfg(test)]
mod tests {
#[test]
fn test_santui_crate_version_exists() {
let version = env!("CARGO_PKG_VERSION");
assert!(!version.is_empty());
let parts: Vec<&str> = version.split('.').collect();
assert_eq!(parts.len(), 3);
}
#[test]
fn test_santui_depends_on_core_crate() {
let _ = santui_core::Santui::new();
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = std::env::args().collect();
if args.iter().any(|a| a == "reset") {
return reset();
}
if args.iter().any(|a| a == "--version" || a == "-V") {
println!("santui v{VERSION}");
return Ok(());
}
if args.iter().any(|a| a == "--help" || a == "-h") {
println!("Santui v{VERSION}");
println!();
println!("Usage: santui [COMMAND]");
println!();
println!("Commands:");
println!(" reset Delete all data and start fresh");
println!();
println!("Flags:");
println!(" --version, -V Print version and exit");
println!(" --list-plugins, plugins List installed/available plugins and exit");
println!(" --help, -h Show this help message");
println!(
" --no-mouse Disable mouse capture at startup (or SANTUI_NO_MOUSE=1)"
);
return Ok(());
}
if args.iter().any(|a| a == "--list-plugins" || a == "plugins") {
return list_plugins();
}
let no_mouse = args.iter().any(|a| a == "--no-mouse")
|| std::env::var("SANTUI_NO_MOUSE").as_deref() == Ok("1");
let dev = std::env::var("SANTUI_DEV").as_deref() == Ok("1");
let dir = if dev {
old_data_dir()
} else {
let dir = santui_db::data_dir();
let old_dir = old_data_dir();
if old_dir != dir && old_dir.exists() && !dir.join("config.toml").exists() {
log::info!("migrating data from {:?} to {:?}", old_dir, dir);
migrate_data_dir(&old_dir, &dir);
}
dir
};
std::fs::create_dir_all(&dir)?;
if !dir.join("config.toml").exists() {
santui_core::config::Config::default().save_to(&dir)?;
}
let cfg = santui_core::config::Config::load_from(&dir);
let sync_client: Option<Arc<SyncClient>> = cfg
.server
.as_ref()
.map(|s| Arc::new(SyncClient::new(s.url.clone())));
let conn = open_db().ok();
let db = Box::new(SantuiDb {
conn,
sync: sync_client.clone(),
});
let mut app = Santui::new();
app.set_db(db);
app.set_sync(sync_client);
app.set_data_dir(dir.clone());
app.set_config_dir(dir);
if no_mouse {
app.set_mouse_capture_startup(false);
}
let mut log_buffer = santui_core::LoggerBuffer::new(10000);
let env_logger = env_logger::Builder::new()
.filter_level(log::LevelFilter::Warn)
.format_timestamp(None)
.format_target(false)
.build();
log_buffer.set_inner(Box::new(env_logger));
let log_buffer_arc = Arc::new(log_buffer);
app.set_log_buffer(log_buffer_arc.clone());
let leaked_arc: &'static Arc<santui_core::LoggerBuffer> = Box::leak(Box::new(log_buffer_arc));
let leaked_logger: &'static santui_core::LoggerBuffer = leaked_arc.as_ref();
let _ = log::set_logger(leaked_logger);
log::set_max_level(log::LevelFilter::Trace);
app.set_plugin_factory(std::sync::Arc::new(santui_ipc::IpcPluginHost::new_boxed));
let exe_dir = std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|d| d.to_path_buf()));
let registry_bin = exe_dir
.as_ref()
.map(|d| {
if cfg!(windows) {
d.join("santui-registry-plugin.exe")
} else {
d.join("santui-registry-plugin")
}
})
.unwrap_or_else(|| PathBuf::from("santui-registry-plugin"));
app.register_default_plugin("plugin-registry", "Plugin Registry", ®istry_bin);
app.set_plugin_persistent("plugin-registry", true);
let log_viewer_bin = exe_dir
.as_ref()
.map(|d| {
if cfg!(windows) {
d.join("santui-log-viewer.exe")
} else {
d.join("santui-log-viewer")
}
})
.unwrap_or_else(|| PathBuf::from("santui-log-viewer"));
let log_consumer_cap = "log-consumer".to_string();
app.register_default_plugin("log-viewer", "Log Viewer", &log_viewer_bin);
app.set_plugin_persistent("log-viewer", true);
app.set_plugin_capabilities("log-viewer", &[log_consumer_cap]);
#[cfg(feature = "auth")]
{
let mut providers = Vec::new();
let github_id = std::env::var("SANTUI_GITHUB_CLIENT_ID")
.unwrap_or_else(|_| "Ov23liQ8S6DliNvkWmoB".into());
let config = santui_auth::AuthConfig::github(github_id);
providers.push(("github".into(), config));
let vercel_url = std::env::var("SANTUI_VERCEL_URL")
.unwrap_or_else(|_| "https://santuiapp.vercel.app".to_string());
let auth: std::sync::Arc<dyn santui_core::AuthHandle> =
std::sync::Arc::new(AuthClient::new(providers).with_vercel(vercel_url));
app.set_auth(auth);
}
app.run()
}