terrazzo-terminal 0.2.8

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

use std::path::Path;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::LazyLock;
use std::sync::Mutex;

use futures::FutureExt as _;
use futures::channel::oneshot;
use futures::future::Shared;
use server_fn::ServerFnError;
use terrazzo::prelude::diagnostics;
use terrazzo::widgets::debounce::DoDebounce;

use self::diagnostics::warn;
use crate::frontend::remotes::Remote;
use crate::text_editor::file_path::FilePath;
use crate::text_editor::side::SideViewNode;
use crate::text_editor::ui::STORE_FILE_DEBOUNCE_DELAY;

pub async fn load_file(
    remote: Remote,
    path: FilePath<Arc<Path>>,
) -> Result<Option<super::File>, ServerFnError> {
    super::load_file(remote, path).await
}

pub async fn load_file_metadata(
    remote: Remote,
    path: FilePath<Arc<Path>>,
) -> Result<Option<super::File>, ServerFnError> {
    super::load_file_metadata(remote, path).await
}

pub async fn list_folder(
    remote: Remote,
    path: FilePath<Arc<Path>>,
) -> Result<Option<Arc<Vec<super::FileMetadata>>>, ServerFnError> {
    super::list_folder(remote, path).await
}

pub async fn file_exists(remote: Remote, path: FilePath<Arc<Path>>) -> Result<bool, ServerFnError> {
    super::file_exists(remote, path).await
}

pub async fn prune_side_view(
    remote: Remote,
    base: Arc<Path>,
    side_view: Option<Arc<SideViewNode<()>>>,
) -> Result<Option<Arc<SideViewNode<()>>>, ServerFnError> {
    let Some(side_view) = side_view else {
        return Ok(None);
    };
    super::prune_side_view(remote, base, side_view).await
}

pub async fn create_file(
    remote: Remote,
    path: FilePath<Arc<Path>>,
    name: String,
) -> Result<(), ServerFnError> {
    super::create_file(remote, path, name).await
}

pub async fn create_folder(
    remote: Remote,
    path: FilePath<Arc<Path>>,
    name: String,
) -> Result<(), ServerFnError> {
    super::create_folder(remote, path, name).await
}

pub async fn move_file(
    remote: Remote,
    source: FilePath<Arc<Path>>,
    destination_folder: FilePath<Arc<Path>>,
) -> Result<(), ServerFnError> {
    super::move_file(remote, source, destination_folder).await
}

pub async fn delete_file(remote: Remote, path: FilePath<Arc<Path>>) -> Result<(), ServerFnError> {
    super::delete_file(remote, path).await
}

static DEBOUNCED_STORE_FILE_FN: LazyLock<StoreFileFn> = LazyLock::new(make_debounced_store_file_fn);
static STORE_FILE_STATE: LazyLock<Mutex<StoreFileState>> = LazyLock::new(Mutex::default);

pub async fn store_file<B: Send + 'static, A: Send + 'static>(
    remote: Remote,
    path: FilePath<Arc<Path>>,
    content: String,
    before: B,
    after: A,
) {
    assert!(std::mem::needs_drop::<B>());
    assert!(std::mem::needs_drop::<A>());
    let debounced_store_file_fn = &*DEBOUNCED_STORE_FILE_FN;
    let done = wait_for_pending_store_file(&remote, &path).await;
    debounced_store_file_fn(StoreFileFnArg {
        remote,
        path,
        content,
        before: Box::new(before),
        after: Box::new(after),
        done,
    })
    .await;
}

async fn wait_for_pending_store_file(
    remote: &Remote,
    path: &FilePath<Arc<Path>>,
) -> oneshot::Sender<()> {
    loop {
        let done = {
            let mut store_file_state = STORE_FILE_STATE.lock().expect("store_file_state");
            match &store_file_state.pending {
                Some(PendingStoreFile {
                    remote: pending_remote,
                    path: pending_path,
                    done,
                }) if pending_remote != remote || pending_path != path => done.clone(),
                _ => {
                    let (done_tx, done_rx) = oneshot::channel();
                    let done: BoxFuture =
                        Box::pin(done_rx.map(|_result: Result<(), oneshot::Canceled>| ()));
                    store_file_state.pending = Some(PendingStoreFile {
                        remote: remote.clone(),
                        path: path.clone(),
                        done: done.shared(),
                    });
                    return done_tx;
                }
            }
        };
        done.await;
    }
}

fn make_debounced_store_file_fn() -> StoreFileFn {
    let debounced = STORE_FILE_DEBOUNCE_DELAY.async_debounce(
        move |StoreFileFnArg {
                  remote,
                  path,
                  content,
                  before,
                  after,
                  done,
              }| async move {
            drop(before);
            let () = super::store_file_impl(remote.clone(), path.clone(), content)
                .await
                .unwrap_or_else(|error| warn!("Failed to store file: {error}"));
            drop(after);
            clear_pending_store_file(&remote, &path);
            let _ = done.send(());
        },
    );
    return Box::new(debounced);
}

fn clear_pending_store_file(remote: &Remote, path: &FilePath<Arc<Path>>) {
    let mut store_file_state = STORE_FILE_STATE.lock().expect("store_file_state");
    if store_file_state
        .pending
        .as_ref()
        .is_some_and(|pending| pending.remote == *remote && pending.path == *path)
    {
        store_file_state.pending = None;
    }
}

type StoreFileFn = Box<dyn Fn(StoreFileFnArg) -> Shared<BoxFuture> + Send + Sync>;
type BoxFuture = Pin<Box<dyn Future<Output = ()> + Send + Sync>>;

struct StoreFileFnArg {
    remote: Remote,
    path: FilePath<Arc<Path>>,
    content: String,
    before: Box<dyn Send>,
    after: Box<dyn Send>,
    done: oneshot::Sender<()>,
}

#[derive(Default)]
struct StoreFileState {
    pending: Option<PendingStoreFile>,
}

struct PendingStoreFile {
    remote: Remote,
    path: FilePath<Arc<Path>>,
    done: Shared<BoxFuture>,
}