#[cfg(not(windows))]
compile_error!("FreeCycle only supports Windows");
pub mod agent_server;
pub mod autostart;
pub mod config;
pub mod exposure_monitor;
pub mod gpu_monitor;
pub mod lockfile;
pub mod logging;
pub mod model_catalog;
pub mod notifications;
pub mod ollama;
pub mod security;
pub mod shortcut;
pub mod state;
pub mod tray;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
pub const REMOTE_MODEL_INSTALL_UNLOCK_DURATION: Duration = Duration::from_secs(60 * 60);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModelTransferKind {
Downloading,
Updating,
}
impl ModelTransferKind {
pub fn label(self) -> &'static str {
match self {
Self::Downloading => "Downloading",
Self::Updating => "Updating",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelProgress {
pub model_name: String,
pub kind: ModelTransferKind,
pub percent: Option<u8>,
pub status_text: String,
pub failed: bool,
}
impl ModelProgress {
pub fn new(model_name: impl Into<String>, kind: ModelTransferKind) -> Self {
Self {
model_name: model_name.into(),
kind,
percent: None,
status_text: kind.label().to_string(),
failed: false,
}
}
pub fn render_status(&self) -> String {
if self.failed {
return self.status_text.clone();
}
match self.percent {
Some(percent) => format!("{} {}: {}%", self.kind.label(), self.model_name, percent),
None if self.status_text.eq_ignore_ascii_case(self.kind.label()) => {
format!("{} {}", self.kind.label(), self.model_name)
}
None => format!(
"{} {}: {}",
self.kind.label(),
self.model_name,
self.status_text
),
}
}
}
pub struct AppState {
pub status: state::FreeCycleStatus,
pub config: config::FreeCycleConfig,
pub agent_task: Option<state::AgentTask>,
pub manual_override: Option<state::ManualOverride>,
pub last_blacklist_seen: Option<std::time::Instant>,
pub vram_idle_since: Option<std::time::Instant>,
pub wake_block_until: Option<std::time::Instant>,
pub remote_model_install_unlocked_until: Option<Instant>,
pub ollama_running: bool,
pub vram_used_bytes: u64,
pub vram_total_bytes: u64,
pub blocking_processes: Vec<String>,
pub local_ip: String,
pub model_progress: Vec<ModelProgress>,
pub model_status: Vec<String>,
pub models_downloading: bool,
pub installed_model_names: Vec<String>,
pub notification_hwnd: Option<isize>,
}
impl AppState {
pub fn new(config: config::FreeCycleConfig) -> Self {
let local_ip = local_ip_address::local_ip()
.map(|ip| ip.to_string())
.unwrap_or_else(|_| "unknown".to_string());
Self {
status: state::FreeCycleStatus::Initializing,
config,
agent_task: None,
manual_override: None,
last_blacklist_seen: None,
vram_idle_since: None,
wake_block_until: None,
remote_model_install_unlocked_until: None,
ollama_running: false,
vram_used_bytes: 0,
vram_total_bytes: 0,
blocking_processes: Vec::new(),
local_ip,
model_progress: Vec::new(),
model_status: Vec::new(),
models_downloading: false,
installed_model_names: Vec::new(),
notification_hwnd: None,
}
}
fn sync_model_status(&mut self) {
self.models_downloading = self.model_progress.iter().any(|progress| !progress.failed);
self.model_status = self
.model_progress
.iter()
.map(ModelProgress::render_status)
.collect();
}
pub fn upsert_model_progress(&mut self, progress: ModelProgress) {
if let Some(existing) = self
.model_progress
.iter_mut()
.find(|existing| existing.model_name == progress.model_name)
{
*existing = progress;
} else {
self.model_progress.push(progress);
}
self.sync_model_status();
}
pub fn remove_model_progress(&mut self, model_name: &str) {
self.model_progress
.retain(|progress| progress.model_name != model_name);
self.sync_model_status();
}
pub fn remote_model_install_unlock_remaining(&self, now: Instant) -> Option<Duration> {
self.remote_model_install_unlocked_until
.and_then(|deadline| deadline.checked_duration_since(now))
}
pub fn remote_model_install_unlocked(&self, now: Instant) -> bool {
self.remote_model_install_unlock_remaining(now).is_some()
}
pub fn clear_expired_remote_model_install_unlock(&mut self, now: Instant) -> bool {
if self.remote_model_install_unlock_remaining(now).is_none()
&& self.remote_model_install_unlocked_until.is_some()
{
self.remote_model_install_unlocked_until = None;
return true;
}
false
}
pub fn enable_remote_model_install_unlock(&mut self, now: Instant) -> Instant {
let deadline = now + REMOTE_MODEL_INSTALL_UNLOCK_DURATION;
self.remote_model_install_unlocked_until = Some(deadline);
deadline
}
pub fn disable_remote_model_install_unlock(&mut self) {
self.remote_model_install_unlocked_until = None;
}
}
pub type SharedAppState = Arc<RwLock<AppState>>;