opt-in-miner 0.4.1

Opt-in Monero/Wownero mining library for transparent application monetization
Documentation
use std::{fs, path::PathBuf};

/// Persisted mining settings (consent, persistence mode, CPU usage, thread count).
///
/// Stored as a plain text file under the platform's config directory
/// (e.g. `~/.config/<app>/mining-settings` on Linux).
pub struct Settings {
    path: PathBuf,
    consent: Status,
    persistence: Persistence,
    cpu_fraction: f32,
    threads: usize,
}

/// Whether the user has granted or denied mining permission.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
    /// The user has granted mining permission.
    Granted,
    /// The user has explicitly denied mining permission.
    Denied,
}

/// Whether to persist mining settings across sessions.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Persistence {
    /// Store settings on disk. The user will not be asked again.
    Save,
    /// Do not store. The user will be asked again next launch and runtime
    /// settings reset to defaults.
    Ask,
}

const DEFAULT_CPU_FRACTION: f32 = 0.25;
const DEFAULT_THREADS: usize = 1;

impl Settings {
    /// Creates a new settings manager for the given application name and
    /// loads any existing values from disk.
    pub fn new(application_name: &str) -> Self {
        let path = dirs_path(application_name);
        let mut settings = Self {
            path,
            consent: Status::Denied,
            persistence: Persistence::Ask,
            cpu_fraction: DEFAULT_CPU_FRACTION,
            threads: DEFAULT_THREADS,
        };
        settings.load();
        settings
    }

    fn load(&mut self) {
        let Ok(content) = fs::read_to_string(&self.path) else {
            return;
        };
        for line in content.lines() {
            let Some((key, value)) = line.split_once(char::is_whitespace) else {
                continue;
            };
            let value = value.trim();
            match key {
                "consent" => {
                    self.consent = match value {
                        "granted" => Status::Granted,
                        _ => Status::Denied,
                    };
                }
                "persistence" => {
                    self.persistence = match value {
                        "save" => Persistence::Save,
                        _ => Persistence::Ask,
                    };
                }
                "cpu_fraction" => {
                    if let Ok(parsed) = value.parse() {
                        self.cpu_fraction = parsed;
                    }
                }
                "threads" => {
                    if let Ok(parsed) = value.parse() {
                        self.threads = parsed;
                    }
                }
                _ => {}
            }
        }
    }

    /// Writes the current settings to disk if [`Self::persistence`] is [`Persistence::Save`].
    /// Removes the file if persistence is [`Persistence::Ask`].
    pub fn save(&self) -> Result<(), std::io::Error> {
        if self.persistence == Persistence::Ask {
            let _ = fs::remove_file(&self.path);
            return Ok(());
        }
        if let Some(parent) = self.path.parent() {
            fs::create_dir_all(parent)?;
        }
        let consent = match self.consent {
            Status::Granted => "granted",
            Status::Denied => "denied",
        };
        let content = format!(
            "consent {consent}\npersistence save\ncpu_fraction {}\nthreads {}\n",
            self.cpu_fraction, self.threads,
        );
        fs::write(&self.path, content)
    }

    pub(crate) fn has_stored(&self) -> bool {
        self.path.exists()
    }

    /// Returns the current consent status.
    pub fn consent(&self) -> Status {
        self.consent
    }

    /// Sets the consent status. Saves to disk if persistence is [`Persistence::Save`].
    pub fn set_consent(&mut self, consent: Status) {
        self.consent = consent;
        let _ = self.save();
    }

    /// Returns the current persistence mode.
    pub fn persistence(&self) -> Persistence {
        self.persistence
    }

    /// Changes the persistence mode. [`Persistence::Save`] persists immediately.
    /// [`Persistence::Ask`] resets runtime values to defaults and removes the file.
    pub fn set_persistence(&mut self, persistence: Persistence) {
        self.persistence = persistence;
        if persistence == Persistence::Ask {
            self.cpu_fraction = DEFAULT_CPU_FRACTION;
            self.threads = DEFAULT_THREADS;
        }
        let _ = self.save();
    }

    /// Returns the stored CPU fraction.
    pub fn cpu_fraction(&self) -> f32 {
        self.cpu_fraction
    }

    /// Sets the CPU fraction. Saves to disk if persistence is [`Persistence::Save`].
    pub fn set_cpu_fraction(&mut self, fraction: f32) {
        self.cpu_fraction = fraction;
        let _ = self.save();
    }

    /// Returns the stored thread count.
    pub fn threads(&self) -> usize {
        self.threads
    }

    /// Sets the thread count. Saves to disk if persistence is [`Persistence::Save`].
    pub fn set_threads(&mut self, threads: usize) {
        self.threads = threads;
        let _ = self.save();
    }
}

fn dirs_path(application_name: &str) -> PathBuf {
    let base = dirs::config_dir().unwrap_or_else(|| PathBuf::from("."));
    base.join(application_name).join("mining-settings")
}

/// The result of a consent check callback.
///
/// Returned by the closure passed to [`crate::MinerBuilder::consent_check`].
pub struct Reply {
    /// Whether the user accepted or denied mining.
    pub consent: Status,
    /// Whether to persist this decision.
    pub persistence: Persistence,
}