Risuko
Architecture

Trait System

How Risuko decouples the engine from runtime environments using Rust traits.

The engine uses three traits to abstract away environment-specific behavior. Each consumer (Tauri, CLI, NAPI) provides its own implementations.

ConfigDirProvider

Controls where configuration and data files are stored.

pub trait ConfigDirProvider: Send + Sync {
    fn config_dir(&self) -> PathBuf;
}

Implementations

ImplementationUsed ByBehavior
DefaultConfigDirCLI, NAPIdirs::config_dir().join("dev.risuko.app")
TauriConfigDirDesktop AppAppHandle::path().app_config_dir()

DefaultConfigDir

pub struct DefaultConfigDir;

impl ConfigDirProvider for DefaultConfigDir {
    fn config_dir(&self) -> PathBuf {
        dirs::config_dir()
            .map(|d| d.join("dev.risuko.app"))
            .unwrap_or_else(|| PathBuf::from("."))
    }
}

Resolves to:

  • macOS: ~/Library/Application Support/dev.risuko.app
  • Linux: ~/.config/dev.risuko.app
  • Windows: C:\Users\<user>\AppData\Roaming\dev.risuko.app

EventSink

Receives engine events and forwards them to the host environment.

pub trait EventSink: Send + Sync {
    fn emit(&self, event: &str, payload: Value);
}

Implementations

ImplementationUsed ByBehavior
NoopEventSinkCLIDiscards events silently
LogEventSinkCLI (verbose)Logs events via log::info!
TauriEventSinkDesktop AppCalls AppHandle::emit() to webview
NAPI callbackNode.jsInvokes JS callback via ThreadsafeFunction

NoopEventSink

pub struct NoopEventSink;

impl EventSink for NoopEventSink {
    fn emit(&self, _event: &str, _payload: Value) {}
}

LogEventSink

pub struct LogEventSink;

impl EventSink for LogEventSink {
    fn emit(&self, event: &str, payload: Value) {
        log::info!("Engine event: {} {}", event, payload);
    }
}

StorageBackend

Persistent key-value storage for RSS data and other state.

pub trait StorageBackend: Send + Sync {
    fn load(&self, key: &str) -> Result<Option<Value>, String>;
    fn save(&self, key: &str, value: &Value) -> Result<(), String>;
}

Implementations

ImplementationUsed ByBehavior
FileStorageCLI, NAPIJSON files in config directory
TauriStorageDesktop AppWraps tauri_plugin_store

FileStorage

pub struct FileStorage {
    dir: PathBuf,
}

impl StorageBackend for FileStorage {
    fn load(&self, key: &str) -> Result<Option<Value>, String> {
        let path = self.dir.join(format!("{key}.json"));
        if !path.exists() { return Ok(None); }
        let data = std::fs::read_to_string(&path)?;
        let value: Value = serde_json::from_str(&data)?;
        Ok(Some(value))
    }

    fn save(&self, key: &str, value: &Value) -> Result<(), String> {
        std::fs::create_dir_all(&self.dir)?;
        let path = self.dir.join(format!("{key}.json"));
        let data = serde_json::to_string_pretty(value)?;
        std::fs::write(&path, data)?;
        Ok(())
    }
}

How the Engine Uses Traits

The EngineManager and RssManager accept trait objects:

// Engine initialization (simplified)
let config_dir = config_provider.config_dir();
let system_config = load_config(&config_dir.join("system.json"));
let user_config = load_config(&config_dir.join("user.json"));
let options = EngineOptions::from_config(&system_config, &user_config);

let manager = TaskManager::new(options, event_sink);
let rss = RssManager::new(storage_backend, event_sink);

Custom Implementations

You can implement these traits to integrate Risuko into other environments:

use risuko_engine::{ConfigDirProvider, EventSink, StorageBackend};

struct MyConfigDir;
impl ConfigDirProvider for MyConfigDir {
    fn config_dir(&self) -> PathBuf {
        PathBuf::from("/my/app/config")
    }
}

struct MyEventSink { /* your state */ }
impl EventSink for MyEventSink {
    fn emit(&self, event: &str, payload: Value) {
        // Send to your event system
    }
}

On this page