hop-core 0.1.0

HOP core — finite-field companion-matrix stream primitives (prototype)
Documentation
use warp::Filter;
use std::sync::{Arc, Mutex};
use futures::{SinkExt, StreamExt};
use warp::ws::{Message, WebSocket};

type Clients = Arc<Mutex<Vec<tokio::sync::mpsc::UnboundedSender<Result<Message, warp::Error>>>>>;

#[tokio::main]
async fn main() {
    let clients: Clients = Arc::new(Mutex::new(Vec::new()));

    // Serve the HTML interface
    let index = warp::path::end().map(|| warp::reply::html(INDEX_HTML));

    // WebSocket endpoint
    let chat = warp::path("chat")
        .and(warp::ws())
        .and(with_clients(clients.clone()))
        .map(|ws: warp::ws::Ws, clients| {
            ws.on_upgrade(move |socket| client_connected(socket, clients))
        });

    let routes = index.or(chat);

    println!("✅ Web chat server running at: http://127.0.0.1:3030");
    println!("🔗 Tunnel it with: ngrok http 3030");
    warp::serve(routes).run(([0, 0, 0, 0], 3030)).await;
}

fn with_clients(clients: Clients) -> impl Filter<Extract = (Clients,), Error = std::convert::Infallible> + Clone {
    warp::any().map(move || clients.clone())
}

async fn client_connected(ws: WebSocket, clients: Clients) {
    let (mut user_ws_tx, mut user_ws_rx) = ws.split();
    let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
    clients.lock().unwrap().push(tx);

    // Sending task
    tokio::task::spawn(async move {
        while let Some(result) = rx.recv().await {
            if let Ok(msg) = result {
                if let Err(e) = user_ws_tx.send(msg).await {
                    eprintln!("Send error: {}", e);
                    break;
                }
            }
        }
    });

    // Receiving loop
    while let Some(result) = user_ws_rx.next().await {
        if let Ok(msg) = result {
            if let Ok(text) = msg.to_str() {
                let clients_guard = clients.lock().unwrap();
                for client in clients_guard.iter() {
                    let _ = client.send(Ok(Message::text(text.to_string())));
                }
            }
        }
    }
}

static INDEX_HTML: &str = r#"<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hop Web Chat</title>
<style>
body {
    font-family: Arial, sans-serif;
    background: #1e1e2f;
    color: #fff;
    margin: 0;
    display: flex;
    flex-direction: column;
    height: 100vh;
}
header {
    background: #27293d;
    padding: 15px;
    text-align: center;
    font-size: 1.4em;
    font-weight: bold;
    border-bottom: 2px solid #3f3f5f;
}
#chat-container {
    flex: 1;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    padding: 10px;
}
#messages {
    flex: 1;
    overflow-y: auto;
    border: 1px solid #3f3f5f;
    background: #2a2a40;
    padding: 10px;
    border-radius: 6px;
}
.message {
    margin-bottom: 8px;
    padding: 6px 10px;
    background: #3b3b5c;
    border-radius: 6px;
    max-width: 75%;
    word-wrap: break-word;
}
.message.self {
    background: #5b5bf0;
    align-self: flex-end;
}
#input-area {
    display: flex;
    margin-top: 10px;
}
#input {
    flex: 1;
    padding: 10px;
    border-radius: 6px;
    border: none;
    outline: none;
    font-size: 1em;
}
button {
    padding: 10px 15px;
    margin-left: 10px;
    background: #5b5bf0;
    color: white;
    border: none;
    border-radius: 6px;
    font-weight: bold;
    cursor: pointer;
}
button:hover {
    background: #7676ff;
}
#username-modal {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0,0,0,0.8);
    display: flex;
    justify-content: center;
    align-items: center;
}
#username-box {
    background: #2a2a40;
    padding: 20px;
    border-radius: 8px;
    text-align: center;
}
#username-input {
    padding: 8px;
    width: 200px;
    border-radius: 6px;
    border: none;
    margin-top: 10px;
    outline: none;
}
</style>
</head>
<body>
<header>💬 Hop Web Chat</header>

<div id="chat-container" style="display:none;">
    <div id="messages"></div>
    <div id="input-area">
        <input id="input" type="text" placeholder="Type your message..." autocomplete="off" />
        <button onclick="sendMessage()">Send</button>
    </div>
</div>

<div id="username-modal">
    <div id="username-box">
        <h2>Enter your nickname</h2>
        <input id="username-input" type="text" placeholder="e.g. LexLuger" />
        <br/><br/>
        <button onclick="setUsername()">Join Chat</button>
    </div>
</div>

<script>
let username = "";
const protocol = location.protocol === "https:" ? "wss" : "ws";
const ws = new WebSocket(`${protocol}://${location.host}/chat`);
const messages = document.getElementById("messages");
const input = document.getElementById("input");

function setUsername() {
    const nameInput = document.getElementById("username-input");
    if (nameInput.value.trim() === "") return alert("Please enter a name");
    username = nameInput.value.trim();
    document.getElementById("username-modal").style.display = "none";
    document.getElementById("chat-container").style.display = "flex";
    ws.send(`🟢 ${username} joined the chat`);
}

ws.onmessage = (event) => {
    const div = document.createElement("div");
    div.classList.add("message");
    div.textContent = event.data;
    messages.appendChild(div);
    messages.scrollTop = messages.scrollHeight;
};

function sendMessage() {
    const text = input.value.trim();
    if (text !== "" && username !== "") {
        const msg = `${username}: ${text}`;
        ws.send(msg);
        const div = document.createElement("div");
        div.classList.add("message", "self");
        div.textContent = msg;
        messages.appendChild(div);
        messages.scrollTop = messages.scrollHeight;
        input.value = "";
    }
}

input.addEventListener("keypress", function(e) {
    if (e.key === "Enter") {
        sendMessage();
        e.preventDefault();
    }
});
</script>
</body>
</html>
"#;