terrazzo-terminal 0.2.7

A simple web-based terminal emulator built on Terrazzo.
use std::iter::once;
use std::time::Duration;

use terrazzo::autoclone;
use terrazzo::declare_trait_aliias;
use terrazzo::html;
use terrazzo::prelude::*;
use terrazzo::template;
use terrazzo::widgets::cancellable::Cancellable;
use terrazzo::widgets::debounce::DoDebounce as _;
use wasm_bindgen_futures::spawn_local;
use web_sys::MouseEvent;

use self::diagnostics::Instrument as _;
use self::diagnostics::debug;
use crate::api::client::remotes_api;
use crate::api::client_address::ClientAddress;

terrazzo_css::import_style!(style, "remotes.scss");

#[derive(Clone)]
pub struct RemotesState {
    pub remotes: XSignal<Remotes>,
    show_remotes: Cancellable<()>,
    hide_remotes: Cancellable<Duration>,
}

pub type Remote = ClientAddress;
pub type Remotes = Option<Vec<ClientAddress>>;

declare_trait_aliias!(
    DisplayRemoteFn,
    Fn(&ClientAddress) -> (String, Option<&'static str>) + Clone + 'static
);

declare_trait_aliias!(ClickRemoteFn, Fn(MouseEvent, Remote) + Clone + 'static);

impl RemotesState {
    pub fn new() -> Self {
        Self {
            remotes: XSignal::new("remotes", None),
            show_remotes: Cancellable::new(),
            hide_remotes: Duration::from_millis(250).cancellable(),
        }
    }

    pub fn show_remotes_dropdown(
        &self,
        display_remote: impl DisplayRemoteFn,
        click: impl ClickRemoteFn,
    ) -> XElement {
        show_remotes_dropdown(
            display_remote,
            click,
            self.remotes.clone(),
            self.hide_remotes.clone(),
        )
    }

    #[autoclone]
    pub fn mouseenter(&self) -> impl Fn(MouseEvent) + 'static {
        let remote_names_state = self.clone();
        move |_| {
            let Self {
                remotes,
                show_remotes,
                hide_remotes,
            } = &remote_names_state;
            show_remotes.cancel();

            let update_remotes = show_remotes.capture(move |new_remotes| {
                autoclone!(remotes);
                remotes.set(new_remotes)
            });
            hide_remotes.cancel();
            let fetch_remotes = async move {
                let remotes = remotes_api::remotes()
                    .await
                    .or_else_throw(|error| format!("Failed to fetch remotes: {error}"));
                if update_remotes(remotes).is_none() {
                    debug!("Updating remotes was canceled");
                }
            };
            spawn_local(fetch_remotes.in_current_span());
        }
    }

    #[autoclone]
    pub fn mouseleave(&self) -> impl Fn(MouseEvent) + 'static {
        let Self {
            remotes,
            hide_remotes,
            ..
        } = self;
        hide_remotes.wrap(move |_| {
            autoclone!(remotes);
            remotes.set(Remotes::None);
        })
    }
}

#[autoclone]
#[html]
#[template(tag = ul)]
fn show_remotes_dropdown(
    display_remote: impl DisplayRemoteFn,
    click: impl ClickRemoteFn,
    #[signal] remotes: Remotes,
    hide_remotes: Cancellable<Duration>,
) -> XElement {
    debug!("Render remote names");
    if let Remotes::Some(remotes) = remotes
        && !remotes.is_empty()
    {
        let local_and_remotes = once(ClientAddress::default()).chain(remotes);
        let remote_names = local_and_remotes.map(|remote| {
            let (remote_name, remote_class) = display_remote(&remote);
            li(
                class = remote_class,
                "{remote_name}",
                mouseenter = move |_ev| {
                    autoclone!(hide_remotes);
                    hide_remotes.cancel();
                },
                click = move |ev| {
                    autoclone!(click);
                    click(ev, remote.clone())
                },
            )
        });
        return tag(class = style::REMOTES_LIST, remote_names..);
    }
    return tag(style::visibility = "hidden", style::display = "none");
}