terrazzo-terminal 0.2.8

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

use std::path::Path;
use std::sync::Arc;

use nameth::NamedEnumValues as _;
use terrazzo::autoclone;
use terrazzo::html;
use terrazzo::prelude::*;
use terrazzo::template;
use terrazzo::widgets::element_capture::ElementCapture;
use web_sys::HtmlInputElement;

use self::diagnostics::debug;
use self::diagnostics::info;
use super::schema::PathSelector;
use crate::assets::icons;
use crate::text_editor::autocomplete::server_fn::AutocompleteItem;
use crate::text_editor::autocomplete::ui::do_autocomplete;
use crate::text_editor::autocomplete::ui::show_autocomplete;
use crate::text_editor::autocomplete::ui::start_autocomplete;
use crate::text_editor::autocomplete::ui::stop_autocomplete;
use crate::text_editor::manager::TextEditorManager;
use crate::text_editor::style;
use crate::utils::more_path::MorePath as _;

impl TextEditorManager {
    pub fn base_path_selector(self: &Ptr<Self>) -> XElement {
        path_selector_impl(
            self.clone(),
            PathSelector::BasePath,
            None,
            self.path.base.clone(),
            self.force_edit_path.clone(),
        )
    }

    pub fn file_path_selector(self: &Ptr<Self>) -> XElement {
        path_selector_impl(
            self.clone(),
            PathSelector::FilePath,
            Some(self.path.base.clone()),
            self.path.file.clone(),
            XSignal::new("unused-force-edit-path", false),
        )
    }
}

#[html]
#[template(tag = div)]
fn path_selector_impl(
    manager: Ptr<TextEditorManager>,
    kind: PathSelector,
    prefix: Option<XSignal<Arc<Path>>>,
    path: XSignal<Arc<Path>>,
    #[signal] mut force_edit_path: bool,
) -> XElement {
    let show_input = kind == PathSelector::FilePath || force_edit_path;
    tag(
        class = style::PATH_SELECTOR,
        style = (!show_input).then_some("width: auto;"),
        img(class = style::PATH_SELECTOR_ICON, src = kind.icon()),
        if show_input {
            path_selector_input(manager, kind, prefix, path)
        } else {
            path_selector_display(kind, path, force_edit_path_mut)
        },
    )
}

#[autoclone]
#[html]
fn path_selector_input(
    manager: Ptr<TextEditorManager>,
    kind: PathSelector,
    prefix: Option<XSignal<Arc<Path>>>,
    path: XSignal<Arc<Path>>,
) -> XElement {
    info!("Show autocomplete input {kind:?}");
    let autocomplete: XSignal<Option<Vec<AutocompleteItem>>> = XSignal::new(kind.name(), None);
    let input: ElementCapture<HtmlInputElement> = ElementCapture::default();
    let do_autocomplete = Ptr::new(do_autocomplete(
        manager.clone(),
        input.clone(),
        autocomplete.clone(),
        kind,
        prefix.clone(),
    ));
    let input_capture = input.capture();
    let onchange = path.add_subscriber(move |new| {
        autoclone!(input);
        let () = input
            .try_with(|i| i.set_value(&new.display().to_string()))
            .unwrap_or_else(|| debug!("input was not set"));
    });
    div(
        class = style::PATH_SELECTOR_WIDGET,
        key = "input",
        input(
            before_render = move |element| {
                let _ = &onchange;
                input_capture(element);
            },
            r#type = "text",
            class = style::PATH_SELECTOR_FIELD,
            #[cfg(not(feature = "client-prod"))]
            class = match kind {
                PathSelector::BasePath => "base-path-selector-field",
                PathSelector::FilePath => "file-path-selector-field",
            },
            focus = start_autocomplete(
                manager.clone(),
                kind,
                prefix.clone(),
                path.clone(),
                input.clone(),
                autocomplete.clone(),
            ),
            blur = stop_autocomplete(kind, path.clone(), input.clone(), autocomplete.clone()),
            keydown = move |_| {
                autoclone!(do_autocomplete);
                do_autocomplete(())
            },
            click = move |_| {
                autoclone!(do_autocomplete);
                do_autocomplete(())
            },
            value = path.get_value_untracked().to_owned_string(),
        ),
        show_autocomplete(
            manager,
            kind,
            prefix.clone(),
            input,
            autocomplete.clone(),
            autocomplete,
            path,
        ),
    )
}

#[html]
#[template(tag = div)]
fn path_selector_display(
    kind: PathSelector,
    #[signal] path: Arc<Path>,
    force_edit_path_mut: MutableSignal<bool>,
) -> XElement {
    #[cfg(feature = "client-prod")]
    let _ = kind;
    let display_path = path.display();
    div(
        class = style::PATH_SELECTOR_WIDGET,
        key = "display",
        span(
            class = style::PATH_SELECTOR_FIELD,
            #[cfg(not(feature = "client-prod"))]
            class = match kind {
                PathSelector::BasePath => "base-path-selector-display",
                PathSelector::FilePath => "file-path-selector-display",
            },
            dblclick = move |_ev| force_edit_path_mut.set(true),
            "{display_path}",
        ),
    )
}

impl PathSelector {
    pub fn icon(self) -> icons::Icon {
        match self {
            Self::BasePath => icons::slash(),
            Self::FilePath => icons::chevron_double_right(),
        }
    }
}