rust rhai ui components wip

This commit is contained in:
Timur Gordon 2025-04-03 19:03:38 +02:00
parent 5764950949
commit 1ea37e2e7f
26 changed files with 3322 additions and 0 deletions

View File

@ -0,0 +1,11 @@
[package]
name = "reloadd"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = "0.21"
notify = "6"
futures-util = "0.3"

View File

@ -0,0 +1,97 @@
use clap::{Parser};
use notify::{RecursiveMode, Watcher, EventKind, recommended_watcher};
use std::sync::{Arc, Mutex};
use std::process::{Command, Child, Stdio};
use tokio::net::TcpListener;
use tokio::sync::broadcast;
use tokio_tungstenite::accept_async;
use futures_util::{SinkExt, StreamExt};
#[derive(Parser, Debug)]
#[command(author, version, about)]
struct Args {
/// Paths to watch (like src/, templates/, scripts/)
#[arg(short, long, value_name = "PATH", num_args = 1.., required = true)]
watch: Vec<String>,
/// Command to run on change (like: -- run --example server)
#[arg(last = true)]
command: Vec<String>,
}
#[tokio::main]
async fn main() {
let args = Args::parse();
let (tx, _) = broadcast::channel::<()>(10);
let tx_ws = tx.clone();
let server_process = Arc::new(Mutex::new(None::<Child>));
// Start WebSocket reload server
tokio::spawn(start_websocket_server(tx_ws.clone()));
// Start watching files and restarting command
let server_process_clone = Arc::clone(&server_process);
let watch_paths = args.watch.clone();
let cmd_args = args.command.clone();
std::thread::spawn(move || {
let mut watcher = recommended_watcher(move |res| {
if let Ok(event) = res {
if matches!(event.kind, EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)) {
println!("📦 Change detected, restarting...");
// Kill previous process
if let Some(mut child) = server_process_clone.lock().unwrap().take() {
let _ = child.kill();
}
// Run new process
let mut cmd = Command::new("cargo");
cmd.args(&cmd_args);
cmd.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
let child = cmd.spawn().expect("Failed to spawn");
*server_process_clone.lock().unwrap() = Some(child);
// Notify browser
let _ = tx.send(());
}
}
}).expect("watcher failed");
// Add watches
for path in &watch_paths {
watcher.watch(path, RecursiveMode::Recursive).unwrap();
}
loop {
std::thread::sleep(std::time::Duration::from_secs(3600));
}
});
println!("🔁 Watching paths: {:?}", args.watch);
println!("🌐 Connect browser to ws://localhost:35729");
loop {
tokio::time::sleep(tokio::time::Duration::from_secs(3600)).await;
}
}
async fn start_websocket_server(tx: broadcast::Sender<()>) {
let listener = TcpListener::bind("127.0.0.1:35729").await.unwrap();
while let Ok((stream, _)) = listener.accept().await {
let tx = tx.clone();
let mut rx = tx.subscribe();
tokio::spawn(async move {
let ws_stream = accept_async(stream).await.unwrap();
let (mut write, _) = ws_stream.split();
while rx.recv().await.is_ok() {
let _ = write.send(tokio_tungstenite::tungstenite::Message::Text("reload".into())).await;
}
});
}
}

View File

@ -0,0 +1,240 @@
use clap::{Parser};
use notify::{RecursiveMode, Watcher, EventKind, recommended_watcher};
use std::sync::{Arc, Mutex};
use std::process::{Command, Child, Stdio};
use tokio::net::TcpStream;
use tokio::sync::broadcast;
use tokio_tungstenite::accept_async;
use futures_util::{SinkExt, StreamExt};
use tokio::time::{sleep, Duration};
use tokio::io::AsyncWriteExt;
use std::net::TcpListener as StdTcpListener;
#[derive(Parser, Debug)]
#[command(author, version, about)]
struct Args {
/// Paths to watch (like src/, templates/, scripts/)
#[arg(short, long, value_name = "PATH", num_args = 1.., required = true)]
watch: Vec<String>,
/// Command to run on change (like: -- run --example server)
#[arg(last = true)]
command: Vec<String>,
}
async fn wait_for_server(port: u16) -> bool {
let address = format!("localhost:{}", port);
let mut retries = 0;
while retries < 10 {
if let Ok(mut stream) = TcpStream::connect(&address).await {
let _ = stream.write_all(b"GET / HTTP/1.1\r\n\r\n").await; // A dummy GET request
println!("✅ Server is ready on {}!", address);
return true;
} else {
retries += 1;
println!("⏳ Waiting for server to be ready (Attempt {}/10)...", retries);
sleep(Duration::from_secs(1)).await;
}
}
eprintln!("❌ Server not ready after 10 attempts.");
false
}
// Check if a port is already in use
fn is_port_in_use(port: u16) -> bool {
StdTcpListener::bind(format!("127.0.0.1:{}", port)).is_err()
}
#[tokio::main]
async fn main() {
let args = Args::parse();
println!("Command: {:?}", args.command);
// Check if server port is already in use
let server_port = 8080; // Adjust as needed
if is_port_in_use(server_port) {
eprintln!("❌ Error: Port {} is already in use. Stop any running instances before starting a new one.", server_port);
std::process::exit(1);
}
// Check if WebSocket port is already in use
let ws_port = 35729;
if is_port_in_use(ws_port) {
eprintln!("❌ Error: WebSocket port {} is already in use. Stop any running instances before starting a new one.", ws_port);
std::process::exit(1);
}
let (tx, _) = broadcast::channel::<()>(10);
let tx_ws = tx.clone();
let server_process = Arc::new(Mutex::new(None::<Child>));
// Validate paths before starting the watcher
let mut invalid_paths = Vec::new();
for path in &args.watch {
if !std::path::Path::new(path).exists() {
invalid_paths.push(path.clone());
}
}
if !invalid_paths.is_empty() {
eprintln!("❌ Error: The following watch paths do not exist:");
for path in invalid_paths {
eprintln!(" - {}", path);
}
eprintln!("Please provide valid paths to watch.");
std::process::exit(1);
}
// Start WebSocket reload server
match start_websocket_server(tx_ws.clone()).await {
Ok(_) => {},
Err(e) => {
eprintln!("❌ Failed to start WebSocket server: {}", e);
std::process::exit(1);
}
}
// 🚀 Run the server immediately
{
let mut cmd = Command::new("cargo");
cmd.args(&args.command);
cmd.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
match cmd.spawn() {
Ok(child) => {
*server_process.lock().unwrap() = Some(child);
},
Err(e) => {
eprintln!("❌ Failed to start initial process: {}", e);
std::process::exit(1);
}
}
}
// Wait for the server to be ready before triggering reloads
if !wait_for_server(server_port).await {
eprintln!("❌ Server failed to start properly.");
std::process::exit(1);
}
println!("🔁 Watching paths: {:?}", args.watch);
println!("🌐 Connect browser to ws://localhost:{}", ws_port);
// Create a runtime handle for the watcher to use
let rt_handle = tokio::runtime::Handle::current();
// Clone necessary values before moving into the watcher thread
let watch_paths = args.watch.clone();
let cmd_args = args.command.clone();
let server_process_clone = Arc::clone(&server_process);
let tx_clone = tx.clone();
// Create a dedicated thread for the file watcher
let watcher_thread = std::thread::spawn(move || {
// Create a new watcher
let mut watcher = match recommended_watcher(move |res: notify::Result<notify::Event>| {
if let Ok(event) = res {
if matches!(event.kind, EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)) {
println!("📦 Change detected, restarting...");
// Kill previous process
if let Some(mut child) = server_process_clone.lock().unwrap().take() {
let _ = child.kill();
}
// Run new process
let mut cmd = Command::new("cargo");
cmd.args(&cmd_args);
cmd.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
match cmd.spawn() {
Ok(child) => {
*server_process_clone.lock().unwrap() = Some(child);
// Use the runtime handle to spawn a task
let tx = tx_clone.clone();
rt_handle.spawn(async move {
if wait_for_server(server_port).await {
// Notify browser to reload
let _ = tx.send(());
}
});
},
Err(e) => {
eprintln!("❌ Failed to spawn process: {}", e);
}
}
}
} else if let Err(e) = res {
eprintln!("❌ Watch error: {}", e);
}
}) {
Ok(w) => w,
Err(e) => {
eprintln!("❌ Failed to create watcher: {}", e);
std::process::exit(1);
}
};
// Add watches
for path in &watch_paths {
match watcher.watch(path.as_ref(), RecursiveMode::Recursive) {
Ok(_) => println!("👁️ Watching path: {}", path),
Err(e) => {
eprintln!("❌ Failed to watch path '{}': {}", path, e);
std::process::exit(1);
}
}
}
// Keep the thread alive
loop {
std::thread::sleep(std::time::Duration::from_secs(3600));
}
});
// Wait for the watcher thread to finish (it shouldn't unless there's an error)
match watcher_thread.join() {
Ok(_) => {},
Err(e) => {
eprintln!("❌ Watcher thread panicked: {:?}", e);
std::process::exit(1);
}
}
}
async fn start_websocket_server(tx: broadcast::Sender<()>) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let listener = tokio::net::TcpListener::bind("127.0.0.1:35729").await?;
println!("WebSocket server started on ws://localhost:35729");
// Spawn a task to handle WebSocket connections
tokio::spawn(async move {
while let Ok((stream, addr)) = listener.accept().await {
println!("New WebSocket connection from: {}", addr);
let tx = tx.clone();
let mut rx = tx.subscribe();
tokio::spawn(async move {
match accept_async(stream).await {
Ok(ws_stream) => {
let (mut write, _) = ws_stream.split();
while rx.recv().await.is_ok() {
if let Err(e) = write.send(tokio_tungstenite::tungstenite::Message::Text("reload".into())).await {
eprintln!("❌ Error sending reload message to client {}: {}", addr, e);
break;
}
}
},
Err(e) => {
eprintln!("❌ Failed to accept WebSocket connection from {}: {}", addr, e);
}
}
});
}
});
Ok(())
}

251
examples/calendar/backend/Cargo.lock generated Normal file
View File

@ -0,0 +1,251 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"const-random",
"getrandom",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bitflags"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[package]]
name = "calendar_backend"
version = "0.1.0"
dependencies = [
"rhai",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "const-random"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
dependencies = [
"const-random-macro",
]
[[package]]
name = "const-random-macro"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
dependencies = [
"getrandom",
"once_cell",
"tiny-keccak",
]
[[package]]
name = "crunchy"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
]
[[package]]
name = "libc"
version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
dependencies = [
"portable-atomic",
]
[[package]]
name = "portable-atomic"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
[[package]]
name = "proc-macro2"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rhai"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6"
dependencies = [
"ahash",
"bitflags",
"instant",
"num-traits",
"once_cell",
"rhai_codegen",
"smallvec",
"smartstring",
"thin-vec",
]
[[package]]
name = "rhai_codegen"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "smallvec"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
[[package]]
name = "smartstring"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
dependencies = [
"autocfg",
"static_assertions",
"version_check",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "syn"
version = "2.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thin-vec"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d"
[[package]]
name = "tiny-keccak"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
dependencies = [
"crunchy",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View File

@ -0,0 +1,11 @@
[package]
name = "calendar_backend"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "rhai_engine"
path = "main.rs"
[dependencies]
rhai = "1.12.0"

View File

@ -0,0 +1,31 @@
{
"calendars": [
{
"id": "cal1",
"name": "Work Calendar",
"owner_id": "user1",
"description": "Main work calendar for team coordination",
"color": "#4285F4",
"shared_with": ["user2", "user3", "user4"],
"visibility": "team"
},
{
"id": "cal2",
"name": "Personal Calendar",
"owner_id": "user1",
"description": "Personal appointments and reminders",
"color": "#0F9D58",
"shared_with": ["user5"],
"visibility": "private"
},
{
"id": "cal3",
"name": "Project Calendar",
"owner_id": "user2",
"description": "Project-specific deadlines and milestones",
"color": "#DB4437",
"shared_with": ["user1", "user3", "user4"],
"visibility": "public"
}
]
}

View File

@ -0,0 +1,57 @@
{
"events": [
{
"id": "event1",
"title": "Team Meeting",
"description": "Weekly team sync meeting",
"start_time": "2025-04-04T10:00:00",
"end_time": "2025-04-04T11:00:00",
"location": "Conference Room A",
"calendar_id": "cal1",
"organizer_id": "user1",
"attendees": ["user1", "user2", "user3"],
"recurring": false,
"status": "confirmed"
},
{
"id": "event2",
"title": "Project Deadline",
"description": "Final submission for Q2 project",
"start_time": "2025-04-15T17:00:00",
"end_time": "2025-04-15T18:00:00",
"location": "Virtual",
"calendar_id": "cal1",
"organizer_id": "user2",
"attendees": ["user1", "user2", "user4"],
"recurring": false,
"status": "confirmed"
},
{
"id": "event3",
"title": "Lunch with Client",
"description": "Discuss upcoming partnership",
"start_time": "2025-04-10T12:30:00",
"end_time": "2025-04-10T14:00:00",
"location": "Downtown Cafe",
"calendar_id": "cal2",
"organizer_id": "user1",
"attendees": ["user1", "user5"],
"recurring": false,
"status": "tentative"
},
{
"id": "event4",
"title": "Weekly Status Update",
"description": "Regular status update meeting",
"start_time": "2025-04-05T09:00:00",
"end_time": "2025-04-05T09:30:00",
"location": "Conference Room B",
"calendar_id": "cal1",
"organizer_id": "user3",
"attendees": ["user1", "user2", "user3", "user4"],
"recurring": true,
"recurrence_pattern": "weekly",
"status": "confirmed"
}
]
}

View File

@ -0,0 +1,54 @@
{
"users": [
{
"id": "user1",
"name": "John Doe",
"email": "john.doe@example.com",
"timezone": "UTC+2",
"preferences": {
"notification_time": 15,
"default_calendar_id": "cal1"
}
},
{
"id": "user2",
"name": "Jane Smith",
"email": "jane.smith@example.com",
"timezone": "UTC+1",
"preferences": {
"notification_time": 30,
"default_calendar_id": "cal1"
}
},
{
"id": "user3",
"name": "Bob Johnson",
"email": "bob.johnson@example.com",
"timezone": "UTC",
"preferences": {
"notification_time": 10,
"default_calendar_id": "cal2"
}
},
{
"id": "user4",
"name": "Alice Brown",
"email": "alice.brown@example.com",
"timezone": "UTC-5",
"preferences": {
"notification_time": 20,
"default_calendar_id": "cal1"
}
},
{
"id": "user5",
"name": "Charlie Davis",
"email": "charlie.davis@example.com",
"timezone": "UTC+3",
"preferences": {
"notification_time": 15,
"default_calendar_id": "cal2"
}
}
]
}

View File

@ -0,0 +1,118 @@
// main.rhai - Main script for the calendar backend
// Import all modules
import "scripts/events" as events;
import "scripts/users" as users;
import "scripts/calendars" as calendars;
import "scripts/utils" as utils;
// Example function to get a user's upcoming events
fn get_user_upcoming_events(user_id, days_ahead) {
let user = users::get_user(user_id);
if utils::is_empty(user) {
return "User not found";
}
// Get current date
let current_date = utils::get_current_date();
// Calculate end date
let end_date = utils::add_days_to_date(current_date, days_ahead);
// Get events in the date range
let upcoming_events = events::get_events_by_date_range(current_date, end_date);
// Filter to only include events where the user is an attendee
let user_events = [];
for event in upcoming_events {
if utils::array_contains(event.attendees, user_id) {
user_events.push(event);
}
}
return user_events;
}
// Example function to get a user's calendar summary
fn get_user_calendar_summary(user_id) {
let user = users::get_user(user_id);
if utils::is_empty(user) {
return "User not found";
}
let user_calendars = calendars::get_accessible_calendars(user_id);
let calendar_count = user_calendars.len();
let user_events = events::get_events_by_attendee(user_id);
let event_count = user_events.len();
let organized_events = events::get_events_by_organizer(user_id);
let organized_count = organized_events.len();
return #{
user_name: user.name,
calendar_count: calendar_count,
event_count: event_count,
organized_count: organized_count
};
}
// Example function to get details of a specific event
fn get_event_details(event_id) {
let event = events::get_event(event_id);
if utils::is_empty(event) {
return "Event not found";
}
let organizer = users::get_user(event.organizer_id);
let calendar = calendars::get_calendar(event.calendar_id);
let attendees_info = [];
for attendee_id in event.attendees {
let attendee = users::get_user(attendee_id);
if !utils::is_empty(attendee) {
attendees_info.push(attendee.name);
}
}
return #{
title: event.title,
description: event.description,
start_time: event.start_time,
end_time: event.end_time,
location: event.location,
organizer: organizer.name,
calendar: calendar.name,
attendees: attendees_info,
status: event.status
};
}
// Example function to create a new event
fn create_new_event(title, description, start_time, end_time, location, calendar_id, organizer_id, attendees) {
// Validate inputs
let calendar = calendars::get_calendar(calendar_id);
if utils::is_empty(calendar) {
return "Calendar not found";
}
let organizer = users::get_user(organizer_id);
if utils::is_empty(organizer) {
return "Organizer not found";
}
// Create the event
let new_event = events::create_event(
title, description, start_time, end_time, location,
calendar_id, organizer_id, attendees, false, "confirmed"
);
return #{
message: "Event created successfully",
event_id: new_event.id,
title: new_event.title
};
}

View File

@ -0,0 +1,11 @@
use rhai::{Engine};
use std::path::{Path, PathBuf};
fn main() -> Result<(), Box<rhai::EvalAltResult>> {
let engine = Engine::new();
// Rhai's import system works relative to current working dir
engine.eval_file::<()>(PathBuf::from("main.rhai"))?;
Ok(())
}

View File

@ -0,0 +1,54 @@
// calendar_model.rhai - Calendar data model
// Create a new calendar object
fn create_calendar(id, name, owner_id, description, color, shared_with, visibility) {
return #{
id: id,
name: name,
owner_id: owner_id,
description: description,
color: color,
shared_with: shared_with,
visibility: visibility
};
}
// Sample calendars data
fn get_sample_calendars() {
let calendars = [];
// Calendar 1: Work Calendar
calendars.push(create_calendar(
"cal1",
"Work Calendar",
"user1",
"Main work calendar for team coordination",
"#4285F4",
["user2", "user3", "user4"],
"team"
));
// Calendar 2: Personal Calendar
calendars.push(create_calendar(
"cal2",
"Personal Calendar",
"user1",
"Personal appointments and reminders",
"#0F9D58",
["user5"],
"private"
));
// Calendar 3: Project Calendar
calendars.push(create_calendar(
"cal3",
"Project Calendar",
"user2",
"Project-specific deadlines and milestones",
"#DB4437",
["user1", "user3", "user4"],
"public"
));
return calendars;
}

View File

@ -0,0 +1,92 @@
// event_model.rhai - Event data model
// Create a new event object
fn create_event(id, title, description, start_time, end_time, location, calendar_id, organizer_id, attendees, recurring, status) {
return #{
id: id,
title: title,
description: description,
start_time: start_time,
end_time: end_time,
location: location,
calendar_id: calendar_id,
organizer_id: organizer_id,
attendees: attendees,
recurring: recurring,
status: status
};
}
// Create a recurring event object (extends regular event)
fn create_recurring_event(id, title, description, start_time, end_time, location, calendar_id, organizer_id, attendees, recurrence_pattern, status) {
let event = create_event(id, title, description, start_time, end_time, location, calendar_id, organizer_id, attendees, true, status);
event.recurrence_pattern = recurrence_pattern;
return event;
}
// Sample events data
fn get_sample_events() {
let events = [];
// Event 1: Team Meeting
events.push(create_event(
"event1",
"Team Meeting",
"Weekly team sync meeting",
"2025-04-04T10:00:00",
"2025-04-04T11:00:00",
"Conference Room A",
"cal1",
"user1",
["user1", "user2", "user3"],
false,
"confirmed"
));
// Event 2: Project Deadline
events.push(create_event(
"event2",
"Project Deadline",
"Final submission for Q2 project",
"2025-04-15T17:00:00",
"2025-04-15T18:00:00",
"Virtual",
"cal1",
"user2",
["user1", "user2", "user4"],
false,
"confirmed"
));
// Event 3: Lunch with Client
events.push(create_event(
"event3",
"Lunch with Client",
"Discuss upcoming partnership",
"2025-04-10T12:30:00",
"2025-04-10T14:00:00",
"Downtown Cafe",
"cal2",
"user1",
["user1", "user5"],
false,
"tentative"
));
// Event 4: Weekly Status Update (recurring)
events.push(create_recurring_event(
"event4",
"Weekly Status Update",
"Regular status update meeting",
"2025-04-05T09:00:00",
"2025-04-05T09:30:00",
"Conference Room B",
"cal1",
"user3",
["user1", "user2", "user3", "user4"],
"weekly",
"confirmed"
));
return events;
}

View File

@ -0,0 +1,72 @@
// user_model.rhai - User data model
// Create user preferences object
fn create_user_preferences(notification_time, default_calendar_id) {
return #{
notification_time: notification_time,
default_calendar_id: default_calendar_id
};
}
// Create a new user object
fn create_user(id, name, email, timezone, preferences) {
return #{
id: id,
name: name,
email: email,
timezone: timezone,
preferences: preferences
};
}
// Sample users data
fn get_sample_users() {
let users = [];
// User 1: John Doe
users.push(create_user(
"user1",
"John Doe",
"john.doe@example.com",
"UTC+2",
create_user_preferences(15, "cal1")
));
// User 2: Jane Smith
users.push(create_user(
"user2",
"Jane Smith",
"jane.smith@example.com",
"UTC+1",
create_user_preferences(30, "cal1")
));
// User 3: Bob Johnson
users.push(create_user(
"user3",
"Bob Johnson",
"bob.johnson@example.com",
"UTC",
create_user_preferences(10, "cal2")
));
// User 4: Alice Brown
users.push(create_user(
"user4",
"Alice Brown",
"alice.brown@example.com",
"UTC-5",
create_user_preferences(20, "cal1")
));
// User 5: Charlie Davis
users.push(create_user(
"user5",
"Charlie Davis",
"charlie.davis@example.com",
"UTC+3",
create_user_preferences(15, "cal2")
));
return users;
}

View File

@ -0,0 +1,73 @@
// calendars.rhai - Functions for handling calendars
import "utils" as utils;
import "../models/calendar_model" as calendar_model;
// Get all calendars
fn get_calendars() {
return calendar_model::get_sample_calendars();
}
// Get a specific calendar by ID
fn get_calendar(calendar_id) {
let calendars = get_all_calendars();
return utils::find_by_id(calendars, calendar_id);
}
// Get calendars owned by a specific user
fn get_calendars_by_owner(user_id) {
let calendars = get_all_calendars();
return utils::filter_by_property(calendars, "owner_id", user_id);
}
// Get calendars shared with a specific user
fn get_calendars_shared_with(user_id) {
let calendars = get_all_calendars();
let shared_calendars = [];
for calendar in calendars {
if utils::array_contains(calendar.shared_with, user_id) {
shared_calendars.push(calendar);
}
}
return shared_calendars;
}
// Get all calendars accessible by a user (owned + shared)
fn get_accessible_calendars(user_id) {
let owned = get_calendars_by_owner(user_id);
let shared_calendars = get_calendars_shared_with(user_id);
// Combine the two arrays
for calendar in shared_calendars {
owned.push(calendar);
}
return owned;
}
// Create a new calendar
fn create_calendar(name, owner_id, description, color, shared_with, visibility) {
// Generate a simple ID (in a real app, we would use a proper ID generation method)
let id = "cal" + (get_all_calendars().len() + 1).to_string();
return calendar_model::create_calendar(
id, name, owner_id, description, color, shared_with, visibility
);
}
// Format calendar details for display
fn format_calendar(calendar) {
if utils::is_empty(calendar) {
return "Calendar not found";
}
let shared_with_count = calendar.shared_with.len();
return `${calendar.name} (${calendar.color})
Description: ${calendar.description}
Owner: ${calendar.owner_id}
Visibility: ${calendar.visibility}
Shared with: ${shared_with_count} users`;
}

View File

@ -0,0 +1,85 @@
// events.rhai - Functions for handling calendar events
import "utils" as utils;
import "../models/event_model" as event_model;
// Get all events
fn get_all_events() {
return event_model::get_sample_events();
}
// Get a specific event by ID
fn get_event(event_id) {
let events = get_all_events();
return utils::find_by_id(events, event_id);
}
// Get events for a specific calendar
fn get_events_by_calendar(calendar_id) {
let events = get_all_events();
return utils::filter_by_property(events, "calendar_id", calendar_id);
}
// Get events for a specific user (as attendee)
fn get_events_by_attendee(user_id) {
let events = get_all_events();
let user_events = [];
for event in events {
if utils::array_contains(event.attendees, user_id) {
user_events.push(event);
}
}
return user_events;
}
// Get events organized by a specific user
fn get_events_by_organizer(user_id) {
let events = get_all_events();
return utils::filter_by_property(events, "organizer_id", user_id);
}
// Get events for a specific date range
fn get_events_by_date_range(start_date, end_date) {
let events = get_all_events();
let filtered_events = [];
for event in events {
let event_start = event.start_time;
// Simple string comparison - in a real app, we would use proper date comparison
if event_start >= start_date && event_start <= end_date {
filtered_events.push(event);
}
}
return filtered_events;
}
// Create a new event
fn create_event(title, description, start_time, end_time, location, calendar_id, organizer_id, attendees, recurring, status) {
// Generate a simple ID (in a real app, we would use a proper ID generation method)
let id = "event" + (get_all_events().len() + 1).to_string();
return event_model::create_event(
id, title, description, start_time, end_time, location,
calendar_id, organizer_id, attendees, recurring, status
);
}
// Format event details for display
fn format_event(event) {
if utils::is_empty(event) {
return "Event not found";
}
let start_date = utils::format_date(event.start_time);
let start_time = utils::format_time(event.start_time);
let end_time = utils::format_time(event.end_time);
return `${event.title} (${start_date}, ${start_time} - ${end_time})
Location: ${event.location}
Description: ${event.description}
Status: ${event.status}`;
}

View File

@ -0,0 +1,60 @@
// users.rhai - Functions for handling calendar users
import "utils" as utils;
import "../models/user_model" as user_model;
// Get all users
fn get_all_users() {
return user_model::get_sample_users();
}
// Get a specific user by ID
fn get_user(user_id) {
let users = get_all_users();
return utils::find_by_id(users, user_id);
}
// Get user by email
fn get_user_by_email(email) {
let users = get_all_users();
for user in users {
if user.email == email {
return user;
}
}
return #{}; // Return empty object if not found
}
// Get users by timezone
fn get_users_by_timezone(timezone) {
let users = get_all_users();
return utils::filter_by_property(users, "timezone", timezone);
}
// Create a new user
fn create_user(name, email, timezone, notification_time, default_calendar_id) {
// Generate a simple ID (in a real app, we would use a proper ID generation method)
let id = "user" + (get_all_users().len() + 1).to_string();
let preferences = user_model::create_user_preferences(
notification_time, default_calendar_id
);
return user_model::create_user(
id, name, email, timezone, preferences
);
}
// Format user details for display
fn format_user(user) {
if utils::is_empty(user) {
return "User not found";
}
return `${user.name} (${user.email})
Timezone: ${user.timezone}
Default Calendar: ${user.preferences.default_calendar_id}
Notification Time: ${user.preferences.notification_time} minutes before`;
}

View File

@ -0,0 +1,84 @@
// utils.rhai - Utility functions for the calendar backend
// Function to find an item by ID in an array
fn find_by_id(array, id) {
for item in array {
if item.id == id {
return item;
}
}
return #{}; // Return empty object if not found
}
// Function to filter array by a property value
fn filter_by_property(array, property, value) {
let result = [];
for item in array {
if item[property] == value {
result.push(item);
}
}
return result;
}
// Function to check if an object is empty
fn is_empty(obj) {
if obj.type_of() == "object" {
return obj.keys().len() == 0;
}
if obj.type_of() == "array" {
return obj.len() == 0;
}
return false;
}
// Function to format date string
fn format_date(date_str) {
// Simple formatting - in a real app, we would use proper date formatting
return date_str.substr(0, 10);
}
// Function to format time string
fn format_time(date_str) {
// Simple formatting - in a real app, we would use proper time formatting
if date_str.len() >= 16 {
return date_str.substr(11, 5);
}
return "";
}
// Function to check if an array contains a value
fn array_contains(array, value) {
for item in array {
if item == value {
return true;
}
}
return false;
}
// Function to get current date (simplified for demo)
fn get_current_date() {
return "2025-04-03";
}
// Function to calculate a future date (simplified for demo)
fn add_days_to_date(date_str, days) {
let year = date_str.substr(0, 4).parse_int();
let month = date_str.substr(5, 2).parse_int();
let day = date_str.substr(8, 2).parse_int() + days;
// Very simplified date calculation - in a real app, we would handle month/year boundaries
if day > 30 {
day = day - 30;
month += 1;
}
if month > 12 {
month = 1;
year += 1;
}
return `${year}-${month.to_string().pad_left(2, '0')}-${day.to_string().pad_left(2, '0')}`;
}

View File

@ -0,0 +1,2 @@
[alias]
dev = "watch -w src -w examples -w src/templates -w src/scripts -x 'run --example server'"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
[package]
name = "calendar"
version = "0.1.0"
edition = "2021"
[dependencies]
hyper = { version = "0.14", features = ["full"] }
tokio = { version = "1", features = ["full"] }
http = "0.2"
httparse = "1"
rhai = { version = "1.21", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tera = "1.0"
once_cell = "1"

View File

@ -0,0 +1,51 @@
# Calendar Server Example
A simple Rust web server using Hyper that exposes an `/all_calendars` endpoint.
## Running the server
```bash
# Navigate to the examples directory
cd /path/to/examples
# Build and run the server
cargo run
```
Once the server is running, you can access the endpoint at:
- http://127.0.0.1:8080/all_calendars
## Features
- Simple HTTP server using Hyper
- Single endpoint that returns "Hello World"
Sure thing! Heres the Markdown version you can copy-paste directly into your README.md:
## 🔁 Live Reload (Hot Reload for Development)
To automatically recompile and restart your example server on file changes (e.g. Rust code, templates, Rhai scripts), you can use [`cargo-watch`](https://github.com/watchexec/cargo-watch):
### ✅ Step 1: Install `cargo-watch`
```bash
cargo install cargo-watch
```
### ✅ Step 2: Run the server with live reload
cargo watch -x 'run --example server'
This will:
• Watch for file changes in your project
• Rebuild and re-run examples/server.rs whenever you make a change
### 🧠 Bonus: Watch additional folders
To also reload when .tera templates or .rhai scripts change:
cargo watch -w src -w examples -w src/templates -w src/scripts -x 'run --example server'
### 💡 Optional: Clear terminal on each reload
cargo watch -c -x 'run --example server'

View File

@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -e
# 🚀 Start dev server with file watching and browser reload
reloadd \
--watch src \
--watch src/templates \
--watch examples \
-- run --example server

View File

@ -0,0 +1,114 @@
use hyper::{Body, Request, Response, Server};
use hyper::service::{make_service_fn, service_fn};
use hyper::{Method, StatusCode};
use std::convert::Infallible;
use std::net::SocketAddr;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::process::Command;
use httparse;
async fn handle_request(req: Request<Body>) -> Result<Response<Body>, Infallible> {
// Serialize request into raw HTTP format
let raw_request = build_raw_http_request(req).await;
// Spawn the binary
let mut child = Command::new("./target/debug/calendar")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()
.expect("failed to spawn binary");
// Feed raw HTTP request to binary's stdin
if let Some(mut stdin) = child.stdin.take() {
let _ = stdin.write_all(&raw_request).await;
}
// Read response from binary's stdout
let mut stdout = child.stdout.take().expect("no stdout");
let mut output = Vec::new();
let _ = stdout.read_to_end(&mut output).await;
let _ = child.wait().await;
// Parse raw HTTP response
match parse_raw_http_response(&output) {
Ok(res) => Ok(res),
Err(e) => {
let mut resp = Response::new(Body::from(format!("Failed to parse response: {}", e)));
*resp.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
Ok(resp)
}
}
}
// Build raw HTTP request string
async fn build_raw_http_request(req: Request<Body>) -> Vec<u8> {
let mut raw = String::new();
let method = req.method();
let path = req.uri().path();
let version = match req.version() {
hyper::Version::HTTP_11 => "HTTP/1.1",
_ => "HTTP/1.1",
};
raw.push_str(&format!("{method} {path} {version}\r\n"));
for (key, value) in req.headers() {
if let Ok(val_str) = value.to_str() {
raw.push_str(&format!("{}: {}\r\n", key, val_str));
}
}
raw.push_str("\r\n");
let body_bytes = hyper::body::to_bytes(req.into_body()).await.unwrap_or_default();
let mut full = raw.into_bytes();
full.extend_from_slice(&body_bytes);
full
}
// Parse raw HTTP response into hyper::Response<Body>
fn parse_raw_http_response(bytes: &[u8]) -> Result<Response<Body>, Box<dyn std::error::Error>> {
let mut headers = [httparse::EMPTY_HEADER; 64];
let mut res = httparse::Response::new(&mut headers);
let parsed_len = res.parse(bytes)?.unwrap(); // returns offset
let status = res.code.unwrap_or(200);
let mut builder = Response::builder().status(status);
for h in res.headers.iter() {
builder = builder.header(h.name, std::str::from_utf8(h.value)?);
}
// Get body and append LiveReload script
let mut body = bytes[parsed_len..].to_vec();
let livereload_snippet = br#"<script>
const ws = new WebSocket("ws://localhost:35729");
ws.onmessage = (msg) => {
if (msg.data === "reload") location.reload();
};
</script>"#;
body.extend_from_slice(b"\n");
body.extend_from_slice(livereload_snippet);
Ok(builder.body(Body::from(body))?)
}
#[tokio::main]
async fn main() {
let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
let make_svc = make_service_fn(|_conn| async {
Ok::<_, Infallible>(service_fn(handle_request))
});
println!("Proxy server running at http://{}", addr);
if let Err(e) = Server::bind(&addr).serve(make_svc).await {
eprintln!("server error: {}", e);
}
}

View File

@ -0,0 +1,121 @@
use rhai::{Engine, Scope, Array, Dynamic, AST};
use hyper::{Request, Body, Response, StatusCode};
use std::collections::HashMap;
use tera::{Function as TeraFunction, Value, to_value, Result as TeraResult};
use tera::Tera;
use std::path::PathBuf;
use serde_json;
use once_cell::sync::Lazy;
pub fn get_calendars(_req: Request<Body>) -> Response<Body> {
let engine = Engine::new();
// Compile the script
let ast = match engine.compile_file(
"/Users/timurgordon/code/git.ourworld.tf/herocode/rhaj/examples/calendar/backend/scripts/calendars.rhai".into()
) {
Ok(ast) => ast,
Err(e) => {
let mut res = Response::new(Body::from(format!("Rhai compile error: {}", e)));
*res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
return res;
}
};
let mut scope = Scope::new();
let tera = setup_tera_with_rhai();
let context = tera::Context::new(); // no need to pass data — we're calling the Rhai function inside
let rendered = match tera.render("get_calendars.html.tera", &context) {
Ok(html) => Response::new(Body::from(html)),
Err(err) => {
// 🔍 Print the full error with source
eprintln!("Template render error: {:?}", err); // or use err.to_string() for cleaner message
let mut res = Response::new(Body::from(format!("Template error: {}", err)));
*res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
res
}
};
rendered
}
// pub fn get_calendars(_req: Request<Body>) -> Response<Body> {
// let engine = Engine::new();
// // Compile the script
// let ast = match engine.compile_file(
// "/Users/timurgordon/code/git.ourworld.tf/herocode/rhaj/examples/calendar/backend/scripts/calendars.rhai".into()
// ) {
// Ok(ast) => ast,
// Err(e) => {
// let mut res = Response::new(Body::from(format!("Rhai compile error: {}", e)));
// *res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
// return res;
// }
// };
// let mut scope = Scope::new();
// // Call the function in the Rhai script
// match engine.call_fn::<Array>(&mut scope, &ast, "get_calendars", ()) {
// Ok(result) => {
// // Convert array to string representation
// let response_text = format!("{:?}", result);
// Response::new(Body::from(response_text))
// },
// Err(e) => {
// let mut res = Response::new(Body::from(format!("Rhai call error: {}", e)));
// *res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
// res
// }
// }
// }
pub struct RhaiFunctionAdapter {
fn_name: String,
script_path: PathBuf,
}
impl TeraFunction for RhaiFunctionAdapter {
fn call(&self, args: &HashMap<String, Value>) -> TeraResult<Value> {
let mut scope = Scope::new();
// Convert args from Tera into Rhai's Dynamic
for (key, value) in args {
let json_str = serde_json::to_string(value).unwrap();
let json_value: serde_json::Value = serde_json::from_str(&json_str).unwrap();
let dynamic = rhai::serde::to_dynamic(json_value).unwrap();
scope.push_dynamic(key.clone(), dynamic);
}
// Build engine and compile AST inside call (safe)
let engine = Engine::new();
let ast = engine
.compile_file(self.script_path.clone())
.map_err(|e| tera::Error::msg(format!("Rhai compile error: {}", e)))?;
let result = engine
.call_fn::<Dynamic>(&mut scope, &ast, &self.fn_name, ())
.map_err(|e| tera::Error::msg(format!("Rhai error: {}", e)))?;
let tera_value = rhai::serde::from_dynamic(&result).unwrap();
Ok(tera_value)
}
}
fn setup_tera_with_rhai() -> Tera {
let mut tera = Tera::new("src/templates/**/*").unwrap();
let adapter = RhaiFunctionAdapter {
fn_name: "get_calendars".into(),
script_path: PathBuf::from("../../backend/scripts/calendars.rhai"),
};
tera.register_function("get_calendars", adapter);
tera
}

View File

@ -0,0 +1,69 @@
mod controller {
pub mod calendar;
}
use controller::calendar;
use hyper::{Body, Method, Request, Response, StatusCode};
use std::convert::Infallible;
use std::io::{self, Read};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let input = read_request_from_stdin()?;
let request = parse_http_request(&input)?;
let response = handle_request(request).await?;
print_http_response(response).await;
Ok(())
}
fn read_request_from_stdin() -> io::Result<String> {
let mut buffer = String::new();
io::stdin().read_to_string(&mut buffer)?;
Ok(buffer)
}
fn parse_http_request(input: &str) -> Result<Request<Body>, Box<dyn std::error::Error>> {
let mut headers = [httparse::EMPTY_HEADER; 32];
let mut req = httparse::Request::new(&mut headers);
let bytes = input.as_bytes();
let parsed_len = req.parse(bytes)?.unwrap();
let method = req.method.ok_or("missing method")?.parse::<Method>()?;
let path = req.path.ok_or("missing path")?;
let uri = path.parse::<hyper::Uri>()?;
let body = &bytes[parsed_len..];
let request = Request::builder()
.method(method)
.uri(uri)
.body(Body::from(body.to_vec()))?;
Ok(request)
}
async fn handle_request(req: Request<Body>) -> Result<Response<Body>, Infallible> {
match (req.method(), req.uri().path()) {
(&Method::GET, "/calendars") => Ok(calendar::get_calendars(req)),
_ => {
let mut res = Response::new(Body::from("Not Found"));
*res.status_mut() = StatusCode::NOT_FOUND;
Ok(res)
}
}
}
async fn print_http_response(res: Response<Body>) {
println!("HTTP/1.1 {}", res.status());
for (key, value) in res.headers() {
println!("{}: {}", key, value.to_str().unwrap_or_default());
}
println!(); // blank line before body
let body_bytes = hyper::body::to_bytes(res.into_body()).await.unwrap();
let body = String::from_utf8_lossy(&body_bytes);
println!("{}", body);
}

View File

@ -0,0 +1,16 @@
<h2>Calendars</h2>
<ul>
{% for cal in get_calendars() %}
<li>
<article>
<header>
<h3>{{ cal.name }}</h3>
<p>{{ cal.description }}</p>
</header>
<footer>
<p>{{ cal.id }}</p>
</footer>
</article>
</li>
{% endfor %}
</ul>