terrazzo-terminal 0.2.7

A simple web-based terminal emulator built on Terrazzo.
use std::path::Path;
use std::sync::Arc;
use std::sync::LazyLock;
use std::time::Duration;

use nameth::NamedEnumValues as _;
use nameth::nameth;
use server_fn::Http;
use server_fn::ServerFnError;
use server_fn::codec::Json;
use terrazzo::server;

use super::file_path::FilePath;
use super::side::SideViewNode;
use crate::api::client_address::ClientAddress;

#[cfg(feature = "server")]
pub mod api;
pub mod client;
mod fsmetadata;
mod git;
mod remote;
mod service;
pub mod ux;

pub static ROOT_BASE_PATH: LazyLock<Arc<Path>> = LazyLock::new(|| Path::new("/").into());
pub static ROOT_FILE_PATH: LazyLock<Arc<Path>> = LazyLock::new(|| Path::new("").into());

#[nameth]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub enum File {
    #[cfg_attr(not(feature = "diagnostics"), serde(rename = "D"))]
    TextFile {
        metadata: Arc<FileMetadata>,
        content: Arc<str>,
        original: Option<Arc<str>>,
    },
    #[cfg_attr(not(feature = "diagnostics"), serde(rename = "P"))]
    PdfFile {
        metadata: Arc<FileMetadata>,
        base64: Arc<str>,
    },
    #[cfg_attr(not(feature = "diagnostics"), serde(rename = "F"))]
    Folder(Arc<Vec<FileMetadata>>),
    #[cfg_attr(not(feature = "diagnostics"), serde(rename = "E"))]
    Error(String),
}

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct FileMetadata {
    #[cfg_attr(not(feature = "diagnostics"), serde(rename = "n"))]
    pub name: Arc<str>,
    #[cfg_attr(not(feature = "diagnostics"), serde(rename = "s"))]
    pub size: Option<u64>,
    #[cfg_attr(not(feature = "diagnostics"), serde(rename = "d"))]
    pub is_dir: bool,
    #[cfg_attr(not(feature = "diagnostics"), serde(rename = "ct"))]
    pub created: Option<Duration>,
    #[cfg_attr(not(feature = "diagnostics"), serde(rename = "at"))]
    pub accessed: Option<Duration>,
    #[cfg_attr(not(feature = "diagnostics"), serde(rename = "mt"))]
    pub modified: Option<Duration>,
    #[cfg_attr(not(feature = "diagnostics"), serde(rename = "m"))]
    pub mode: Option<u32>,
    #[cfg_attr(not(feature = "diagnostics"), serde(rename = "u"))]
    pub user: Option<Arc<str>>,
    #[cfg_attr(not(feature = "diagnostics"), serde(rename = "g"))]
    pub group: Option<Arc<str>>,
}

impl std::fmt::Debug for File {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let mut f = f.debug_tuple(self.name());
        match self {
            Self::TextFile { content, .. } => f.field(&content.len()),
            Self::PdfFile { base64, .. } => f.field(&base64.len()),
            Self::Folder(folder) => f.field(&folder.len()),
            Self::Error(error) => f.field(error),
        }
        .finish()
    }
}

#[server(protocol = Http<Json, Json>)]
#[nameth]
async fn load_file(
    remote: ClientAddress,
    path: FilePath<Arc<Path>>,
) -> Result<Option<File>, ServerFnError> {
    Ok(remote::LOAD_FILE_REMOTE_FN
        .call(remote, remote::LoadFileRequest { path })
        .await?)
}

#[server(protocol = Http<Json, Json>)]
#[nameth]
async fn load_file_metadata(
    remote: ClientAddress,
    path: FilePath<Arc<Path>>,
) -> Result<Option<File>, ServerFnError> {
    Ok(remote::LOAD_FILE_METADATA_REMOTE_FN
        .call(remote, remote::LoadFileRequest { path })
        .await?)
}

#[server(protocol = Http<Json, Json>)]
#[nameth]
async fn list_folder(
    remote: ClientAddress,
    path: FilePath<Arc<Path>>,
) -> Result<Option<Arc<Vec<FileMetadata>>>, ServerFnError> {
    Ok(remote::LIST_FOLDER_REMOTE_FN
        .call(remote, remote::ListFolderRequest { path })
        .await?)
}

#[server(protocol = Http<Json, Json>)]
#[nameth]
async fn file_exists(
    remote: ClientAddress,
    path: FilePath<Arc<Path>>,
) -> Result<bool, ServerFnError> {
    Ok(remote::FILE_EXISTS_REMOTE_FN
        .call(remote, remote::FileExistsRequest { path })
        .await?)
}

#[server(protocol = Http<Json, Json>)]
#[nameth]
async fn prune_side_view(
    remote: ClientAddress,
    base: Arc<Path>,
    node: Arc<SideViewNode<()>>,
) -> Result<Option<Arc<SideViewNode<()>>>, ServerFnError> {
    Ok(remote::PRUNE_SIDE_VIEW_REMOTE_FN
        .call(remote, remote::PruneSideViewRequest { base, node })
        .await?)
}

#[server(protocol = Http<Json, Json>)]
#[nameth]
async fn create_file(
    remote: ClientAddress,
    path: FilePath<Arc<Path>>,
    name: String,
) -> Result<(), ServerFnError> {
    Ok(remote::CREATE_FILE_REMOTE_FN
        .call(remote, remote::CreateEntryRequest { path, name })
        .await?)
}

#[server(protocol = Http<Json, Json>)]
#[nameth]
async fn create_folder(
    remote: ClientAddress,
    path: FilePath<Arc<Path>>,
    name: String,
) -> Result<(), ServerFnError> {
    Ok(remote::CREATE_FOLDER_REMOTE_FN
        .call(remote, remote::CreateEntryRequest { path, name })
        .await?)
}

#[server(protocol = Http<Json, Json>)]
#[nameth]
async fn move_file(
    remote: ClientAddress,
    source: FilePath<Arc<Path>>,
    destination_folder: FilePath<Arc<Path>>,
) -> Result<(), ServerFnError> {
    Ok(remote::MOVE_FILE_REMOTE_FN
        .call(
            remote,
            remote::MoveFileRequest {
                source,
                destination_folder,
            },
        )
        .await?)
}

#[server(protocol = Http<Json, Json>)]
#[nameth]
async fn delete_file(
    remote: ClientAddress,
    path: FilePath<Arc<Path>>,
) -> Result<(), ServerFnError> {
    Ok(remote::DELETE_FILE_REMOTE_FN
        .call(remote, remote::DeleteFileRequest { path })
        .await?)
}

#[server(protocol = Http<Json, Json>)]
#[nameth]
async fn store_file_impl(
    remote: ClientAddress,
    path: FilePath<Arc<Path>>,
    content: String,
) -> Result<(), ServerFnError> {
    #[cfg(debug_assertions)]
    tokio::time::sleep(std::time::Duration::from_secs(1)).await;
    Ok(remote::STORE_FILE_REMOTE_FN
        .call(remote, remote::StoreFileRequest { path, content })
        .await?)
}