terrazzo-terminal 0.2.7

A simple web-based terminal emulator built on Terrazzo.
#![cfg(feature = "client")]

use std::sync::Arc;
use std::sync::Mutex;

use terrazzo::autoclone;
use terrazzo::envelope;
use terrazzo::prelude::XSignal;
use terrazzo::prelude::diagnostics;
use wasm_bindgen_futures::spawn_local;

use self::diagnostics::warn;
use super::schema::PortForward;
use super::sync_state::Fields;
use super::sync_state::SyncState;
use crate::api::client::remotes_api;
use crate::api::client_address::ClientAddress;
use crate::frontend::remotes::Remote;
use crate::tiles::signals::TilePtr;

/// The manager for the port forward feature.
#[envelope]
pub struct ManagerImpl {
    tile: TilePtr,

    /// The port forwards as displayed in the UI.
    ///
    /// It is updated after the state is updated in the backend.
    port_forwards_signal: XSignal<Arc<Vec<PortForward>>>,

    /// The port forwards state. It is set after the backend has processed the update.
    port_forwards: Mutex<Arc<Vec<PortForward>>>,

    remotes: XSignal<Vec<ClientAddress>>,
}

pub use ManagerImplPtr as Manager;

impl Manager {
    #[autoclone]
    pub fn new(tile: TilePtr) -> Self {
        let manager = Self::from(ManagerImpl {
            tile,
            port_forwards_signal: XSignal::new("port-forwards", Default::default()),
            port_forwards: Mutex::default(),
            remotes: XSignal::new("remotes", vec![]),
        });

        // TODO: Show the remotes accessible from the selected remote.
        spawn_local(async move {
            autoclone!(manager);
            let Ok(remotes) = remotes_api::remotes().await else {
                return;
            };
            manager.remotes.set(remotes);
        });
        return manager;
    }

    pub fn tile(&self) -> TilePtr {
        self.tile.clone()
    }
    pub fn remotes(&self) -> XSignal<Vec<ClientAddress>> {
        self.remotes.clone()
    }
    pub fn port_forwards(&self) -> &XSignal<Arc<Vec<PortForward>>> {
        &self.port_forwards_signal
    }
    pub fn port_forwards_lock(&self) -> std::sync::MutexGuard<'_, Arc<Vec<PortForward>>> {
        self.port_forwards.lock().expect("port_forwards lock")
    }

    /// Loads the port forwards from the backend server into the UI state.
    pub fn load_port_forwards(&self, remote: Remote) {
        let manager = self.clone();
        spawn_local(async move {
            let Ok(port_forwards) = super::state::load_port_forwards(remote).await else {
                return;
            };
            let port_forwards: Arc<Vec<PortForward>> = Arc::from(port_forwards);
            *manager.port_forwards_lock() = port_forwards.clone();
            manager.port_forwards().set(port_forwards);
        });
    }

    /// Updates a field of a given port forward.
    ///
    /// * `remote` - The terrazzo server for which to update port forwards.
    /// * `sync_state` - The synchronization state for the [PortForward].
    /// * `id` - The ID of the port forward to update.
    /// * `field` - The field to update.
    /// * `update_fn` - The function that takes the current port forward and returns the updated port forward.
    ///   If it returns `None`, the port forward is removed.
    pub fn set(
        &self,
        remote: &Remote,
        sync_state: XSignal<SyncState>,
        id: i32,
        field: Fields,
        update_fn: impl FnOnce(&PortForward) -> Option<PortForward>,
    ) {
        let mut update_fn = Some(update_fn);
        self.update(remote, sync_state, field, move |port_forwards| {
            port_forwards
                .iter()
                .filter_map(|port_forward| {
                    if port_forward.id == id {
                        let update_fn = update_fn.take().unwrap();
                        update_fn(port_forward)
                    } else {
                        Some(port_forward.clone())
                    }
                })
                .collect::<Vec<_>>()
                .into()
        });
    }

    /// Runs the update routine for a port forward update.
    ///
    /// Keeps the UI state up to date as the state is updated in the backend.
    #[autoclone]
    pub fn update(
        &self,
        remote: &Remote,
        sync_state: XSignal<SyncState>,
        field: Fields,
        update_fn: impl FnOnce(&Arc<Vec<PortForward>>) -> Arc<Vec<PortForward>>,
    ) {
        let loading = SyncState::incr_loading(sync_state, field);
        let mut port_forwards_lock = self.port_forwards_lock();
        let new = update_fn(&port_forwards_lock);
        *port_forwards_lock = new.clone();
        drop(port_forwards_lock);

        let this = self.clone();
        spawn_local(async move {
            autoclone!(remote);
            let Ok(()) = super::state::store_port_forwards(remote.clone(), new.clone())
                .await
                .inspect_err(|error| warn!("Failed to save port forwards: {error}"))
            else {
                return;
            };
            let new = match super::state::load_port_forwards(remote).await {
                Ok(new) => new.into(),
                Err(error) => {
                    warn!("Failed to load port forwards: {error}");
                    new
                }
            };
            this.port_forwards().set(new);
            drop(loading);
        })
    }
}