gled 2.28.5

gled is an application for creating animations and effects on artnet or dmx installations
pub mod action;
pub mod asset;
pub mod asset_id;
pub mod collection;
pub mod collections;
pub mod git;
pub mod serde;

use self::{action::StorageAction, asset::*, asset_id::AssetId};
use crate::{
    app::persistent_state::PersistentState, storage::collections::Collections, ui::action::UiAction,
};
use animation::Animation;
use collection::Collection;
use curve::Curve;
use directories::BaseDirs;
use egui::mutex::Mutex;
use git::Git;
use midi_controller::MidiController;
use once_cell::sync::Lazy;
use output_device::OutputDevice;
use palette::Palette;
use project::Project;
use scene::Scene;
use std::{
    fmt::Debug,
    fs::{read_to_string, remove_dir_all},
    path::PathBuf,
    sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed},
    thread::sleep,
    time::Duration,
};
use strum::Display;
use uuid::Uuid;

pub static STORAGE_DIR: Lazy<PathBuf> = Lazy::new(|| {
    BaseDirs::new()
        .expect("Could not get base dirs")
        .data_dir()
        .join("gled2")
});
static STORAGE_VERSION_FILE: Lazy<PathBuf> = Lazy::new(|| STORAGE_DIR.join("version"));
static WORKING: AtomicBool = AtomicBool::new(true);
static ERROR: Lazy<Mutex<Option<String>>> = Lazy::new(|| Mutex::new(None));
static LOADING: AtomicLoading = AtomicLoading::new(Loading::No);
static BRANCHES: Lazy<Mutex<Option<Branches>>> = Lazy::new(|| Mutex::new(None));
static STAGED_FILES: AtomicUsize = AtomicUsize::new(0);
#[derive(Debug, Clone)]
pub struct Branches {
    pub available: Vec<String>,
    pub current: String,
}

#[derive(Display, Default, PartialEq, Eq)]
#[atomic_enum::atomic_enum]
pub enum Loading {
    #[default]
    #[strum(serialize = "Idling")]
    No,
    #[strum(serialize = "Nuking storage")]
    Nuking,
    #[strum(serialize = "Loading git repository")]
    GitRepository,
    #[strum(serialize = "Loading git branches")]
    GitBranches,
    #[strum(serialize = "Loading current git branch")]
    GitBranch,
    #[strum(serialize = "Loading animations")]
    Animations,
    #[strum(serialize = "Loading curves")]
    Curves,
    #[strum(serialize = "Loading output devices")]
    OutputDevices,
    #[strum(serialize = "Loading MIDI controllers")]
    MidiControllers,
    #[strum(serialize = "Loading palettes")]
    Palettes,
    #[strum(serialize = "Loading projects")]
    Projects,
    #[strum(serialize = "Loading scenes")]
    Scenes,
    #[strum(serialize = "Synchronizing")]
    Synchronizing,
}

impl Loading {
    pub fn set(self) {
        LOADING.store(self, Relaxed);
    }
}

pub fn working() -> bool {
    WORKING.load(Relaxed)
}

pub fn staged_files() -> usize {
    STAGED_FILES.load(Relaxed)
}

pub fn loading() -> Loading {
    LOADING.load(Relaxed)
}

pub fn is_loading() -> bool {
    loading() != Loading::No
}

pub fn error() -> Option<String> {
    ERROR.lock().clone()
}

pub fn branches() -> Option<Branches> {
    BRANCHES.lock().clone()
}

pub fn start_thread() {
    let actions = action::init();

    std::thread::Builder::new()
        .name("gled:storage".to_string())
        .spawn(move || {
            #[cfg(feature = "profiling")]
            profiling::register_thread!("storage");

            let mut retry_wait = std::time::Duration::from_secs(0);
            loop {
                sleep(retry_wait);
                retry_wait = std::time::Duration::from_secs(2);

                ERROR.lock().take();

                // Clear the queue
                while let Ok(Some(_)) = actions.try_recv() {}

                Loading::GitRepository.set();
                let mut git = match git::Git::open(PersistentState::default().git_url()) {
                    Ok(git) => git,
                    Err(err) => {
                        let err: String = format!("Could not open git: {err}");
                        tracing::error!("{err}");
                        ERROR.lock().replace(err);
                        continue;
                    }
                };

                StorageAction::LoadBranches.enqueue();
                StorageAction::CountStagedFiles.enqueue();
                StorageAction::LoadAssets.enqueue();

                while let Ok(action) = actions.recv() {
                    WORKING.store(true, Relaxed);

                    match action {
                        StorageAction::Nuke => {
                            ERROR.lock().take();
                            Loading::Nuking.set();
                            remove_dir_all(&*STORAGE_DIR).ok();
                            StorageAction::Restart.enqueue();
                        }
                        StorageAction::Restart => {
                            break;
                        }
                        StorageAction::Stop => {
                            return;
                        }
                        StorageAction::CountStagedFiles => match git.count_staged_files() {
                            Err(err) => {
                                ERROR
                                    .lock()
                                    .replace(format!("Error counting staged files: {err}"));
                                sleep(Duration::from_secs(1));
                                StorageAction::CountStagedFiles.enqueue();
                            }
                            Ok(count) => {
                                STAGED_FILES.store(count, Relaxed);
                                ERROR.lock().take();
                            }
                        },
                        StorageAction::Pull => {
                            if let Err(err) = git.pull() {
                                ERROR.lock().replace(format!("Error pulling: {err}"));
                                sleep(Duration::from_secs(1));
                                StorageAction::Pull.enqueue();
                            } else {
                                ERROR.lock().take();
                            }
                        }
                        StorageAction::Push => {
                            if let Err(err) = git.push() {
                                ERROR.lock().replace(format!("Error pushing: {err}"));
                                sleep(Duration::from_secs(1));
                                StorageAction::Push.enqueue();
                            } else {
                                ERROR.lock().take();
                            }
                        }
                        StorageAction::Commit { message } => {
                            if let Err(err) = git.commit(&message) {
                                ERROR
                                    .lock()
                                    .replace(format!("Error committing and pushing: {err}"));
                                sleep(Duration::from_secs(1));
                                StorageAction::Commit { message }.enqueue();
                            } else {
                                ERROR.lock().take();
                            }
                            StorageAction::CountStagedFiles.enqueue();
                        }
                        StorageAction::LoadBranches => {
                            BRANCHES.lock().take();

                            Loading::GitBranches.set();
                            let branches = match git.branches() {
                                Ok(branches) => branches,
                                Err(err) => {
                                    let err: String = format!("Could not get branches: {err}");
                                    tracing::error!("{err}");
                                    ERROR.lock().replace(err);
                                    continue;
                                }
                            };

                            Loading::GitBranch.set();
                            let current_branch = match git.current_branch() {
                                Ok(current_branch) => current_branch,
                                Err(err) => {
                                    let err: String = format!("Could not get current_branch: {err}");
                                    tracing::error!("{err}");
                                    ERROR.lock().replace(err);
                                    continue;
                                }
                            };

                            *BRANCHES.lock() = Some(Branches {
                                available: branches,
                                current: current_branch,
                            });

                            ERROR.lock().take();
                        }
                        StorageAction::LoadAssets => {
                            let version_file = STORAGE_DIR.join("version");
                            match read_to_string(version_file).ok().and_then(|version| {
                                semver::Version::parse(version.trim())
                                    .map_err(|err| tracing::error!("Could not parse version file: {err:?}"))
                                    .ok()
                            }) {
                                Some(version) => {
                                    if version
                                        > semver::Version::parse(env!("CARGO_PKG_VERSION"))
                                            .expect("Could not parse cargo pkg version")
                                    {
                                        tracing::error!(
                                            "Gled version is too old. Please update to the latest version."
                                        );
                                        ERROR.lock().replace("Gled version is too old. Please update to the latest version.".to_string());
                                        continue;
                                    }
                                }
                                None => {
                                    write_storage_version_file(&mut git);
                                }
                            }

                            if let Err(err) = mkdirp::mkdirp(STORAGE_DIR.join("svg")) {
                                ERROR
                                    .lock()
                                    .replace(format!("Could not create svg templates folder: {err}"));
                                continue;
                            }

                            let mut collections = Collections::default();

                            Loading::Animations.set();
                            collections.insert::<Animation>(Collection::<Animation>::load());

                            Loading::Curves.set();
                            collections.insert::<Curve>(Collection::<Curve>::load());

                            Loading::OutputDevices.set();
                            collections.insert::<OutputDevice>(Collection::<OutputDevice>::load());

                            Loading::MidiControllers.set();
                            collections.insert::<MidiController>(Collection::<MidiController>::load());

                            Loading::Palettes.set();
                            collections.insert::<Palette>(Collection::<Palette>::load());

                            Loading::Projects.set();
                            collections.insert::<Project>(Collection::<Project>::load());

                            Loading::Scenes.set();
                            collections.insert::<Scene>(Collection::<Scene>::load());

                            Loading::Synchronizing.set();
                            collections.save();

                            Loading::No.set();
                        }
                        StorageAction::SwitchBranch(branch) => match git.switch_branch(&branch) {
                            Ok(_) => {
                                StorageAction::Restart.enqueue();
                                StorageAction::LoadBranches.enqueue();
                                StorageAction::CountStagedFiles.enqueue();
                                StorageAction::LoadAssets.enqueue();
                            }
                            Err(err) => {
                                ERROR
                                    .lock()
                                    .replace(format!("Error switching branch: {err}"));
                            }
                        },
                        StorageAction::SaveAsset {
                            dir_name,
                            uuid,
                            json,
                        } => {
                            if let Err(err) = git.write_asset(&asset_path(uuid, dir_name), json) {
                                UiAction::Error(format!("Could not write asset: {err}")).enqueue();
                            }

                            write_storage_version_file(&mut git);

                            StorageAction::CountStagedFiles.enqueue();
                        }
                        StorageAction::DeleteAsset { uuid, dir_name } => {
                            if let Err(err) = git.delete_asset(&asset_path(uuid, dir_name)) {
                                tracing::error!("Could not delete asset: {err:?}");
                            }

                            StorageAction::CountStagedFiles.enqueue();
                        }
                    }

                    WORKING.store(false, Relaxed);
                }
            }
        })
    .expect("Could not spawn storage thread");
}

pub fn asset_path(id: Uuid, dir_name: &str) -> PathBuf {
    STORAGE_DIR.join(dir_name).join(format!("{id}.json"))
}

pub fn write_storage_version_file(git: &mut Git) {
    static STORAGE_VERSION_WRITTEN: AtomicBool = AtomicBool::new(false);

    if STORAGE_VERSION_WRITTEN.load(Relaxed) {
        return;
    }

    if let Err(err) = std::fs::write(
        STORAGE_VERSION_FILE.as_path(),
        format!("{}\n", env!("CARGO_PKG_VERSION")),
    ) {
        tracing::error!("Could not write storage version file: {err}");
        ERROR
            .lock()
            .replace(format!("Could not write storage version file: {err}"));
        StorageAction::Stop.enqueue();
        return;
    }

    if let Err(err) = git.add(STORAGE_VERSION_FILE.as_path()) {
        tracing::error!("Could not add storage version file to git: {err}");
        ERROR
            .lock()
            .replace(format!("Could not add storage version file to git: {err}"));
        StorageAction::Stop.enqueue();
        return;
    }

    STORAGE_VERSION_WRITTEN.store(true, Relaxed);
}