assistant_daemon 0.1.0

Daemon program for providing many features.
#![feature(async_closure)]
#![feature(lazy_cell)]

use anyhow::Result;
use axum::{
    extract::State,
    routing::{get, post},
    Extension, Json, Router,
};
use config::{ConfigManager, FsConfigManager};
use generated::{
    FeatureUsecase, GetFeatureStatusRequest, GetFeatureStatusResponse, GetFeaturesResponse,
    ToggleFeatureRequest, ToggleFeatureResponse, WallpaperUsecase,
};
use serde_json::Value;
use std::{
    net::SocketAddr,
    path::{Path, PathBuf},
    sync::Arc,
};
use tokio::net::TcpListener;
use tracing::{error, info, level_filters::LevelFilter, Level};

use crate::{
    feature::{FeatureManager, FeatureManagerImpl},
    features::FeatureUsecaseImpl,
    generated::Config,
};

#[cfg(target_os = "macos")]
mod laptoplid;
#[cfg(target_os = "macos")]
use crate::laptoplid::{turn_off_laptoplid_change, turn_on_laptoplid_change};

mod config;
mod feature;
mod features;
mod generated;

// the application state
#[derive(Clone)]
struct AppState {
    feature_usecase: Arc<dyn FeatureUsecase + Sync + Send>,
}

impl AppState {
    fn new(feature_usecase: Arc<dyn FeatureUsecase + Sync + Send>) -> Self {
        Self { feature_usecase }
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    let config_dir = config::get_config_dir();
    config::init_dir(&config_dir)?;
    let log_file = config_dir.join("log");
    // setup logging
    let _guard = assistant_common::setup_logging(Some(log_file))?;

    // config manager
    let cfg_mngr: Arc<FsConfigManager<serde_json::Value>> =
        Arc::new(FsConfigManager::new(config_dir, "settings.json".to_owned()));

    // feature manager
    let ft_mngt = Arc::new(FeatureManagerImpl::new());

    // features
    let wp_usecase = Arc::new(features::WallpaperUsecaseImpl::new());
    ft_mngt.register(wp_usecase).await;

    let sshmp_usecase = Arc::new(features::SshpmUsecaseImpl::new());
    ft_mngt.register(sshmp_usecase).await;

    #[cfg(target_os = "windows")]
    {
        let wslpf_usecase = Arc::new(features::WslpfUsecaseImpl::new());
        ft_mngt.register(wslpf_usecase).await;
    }

    // FIXME: too ugly, any workaround?
    let ft_mngt1 = ft_mngt.clone();
    cfg_mngr.on(Arc::new(move |oldc, newc| {
        let ft_mngt2 = ft_mngt1.clone();
        Box::pin(async move {
            #[cfg(target_os = "macos")]
            match newc
                .get("detect_lidchange")
                .unwrap_or(&Value::Bool(false))
                .as_bool()
            {
                Some(should_listen) => {
                    if should_listen {
                        match turn_on_laptoplid_change(
                            ft_mngt2.clone(),
                            tokio::runtime::Handle::current(),
                        ) {
                            Ok(_) => {}
                            Err(err) => {
                                error!("failed to turn on laptoplid change detection: {}", err);
                            }
                        }
                    } else {
                        match turn_off_laptoplid_change() {
                            Ok(_) => {}
                            Err(err) => {
                                error!("failed to turn off laptoplid change detection: {}", err);
                            }
                        }
                    }
                }
                None => {}
            }

            ft_mngt2.update_config(oldc, newc).await;
        })
    }));

    // after setting up all the listeners in config manager
    cfg_mngr
        .init(Some(serde_json::to_value(Config::default())?))
        .await?;
    cfg_mngr.set_fs_watch(true).await?;
    let feature_usecase = Arc::new(FeatureUsecaseImpl::new(cfg_mngr));

    let app_state = AppState::new(feature_usecase);

    // Set up the server address
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

    // Create a TCP listener
    let listener = TcpListener::bind(&addr).await?;
    info!("server is listening on port {}", addr);

    // Set up the router
    let app = Router::new()
        .route("/toggle_feature", post(toggle_feature_handler))
        .route("/get_features", get(get_features_handler))
        .route("/get_feature_status", post(get_feature_status_handler))
        .with_state(app_state);

    axum::serve(listener, app).await.unwrap();
    Ok(())
}

async fn toggle_feature_handler(
    State(state): State<AppState>,
    Json(request): Json<ToggleFeatureRequest>,
) -> Json<ToggleFeatureResponse> {
    let response = state.feature_usecase.toggle_feature(request).await.unwrap();
    Json(response)
}

async fn get_features_handler(State(state): State<AppState>) -> Json<GetFeaturesResponse> {
    let response = state.feature_usecase.get_features().await.unwrap();
    Json(response)
}

async fn get_feature_status_handler(
    State(state): State<AppState>,
    Json(request): Json<GetFeatureStatusRequest>,
) -> Json<GetFeatureStatusResponse> {
    let response = state
        .feature_usecase
        .get_feature_status(request)
        .await
        .unwrap();
    Json(response)
}