use std::env; use std::sync::Mutex; #[macro_use] extern crate rocket; use rocket::State; use rocket::form::Form; use rocket::response::content::RawHtml; use maud::{html, DOCTYPE}; #[derive(Clone)] struct Todo { id: u32, title: String, completed: bool, } /// In-memory store for todos, protected by a Mutex for thread-safe access. /// Rocket handlers access it via `&State`. struct TodoStore { todos: Mutex>, next_id: Mutex, } impl TodoStore { fn new() -> Self { TodoStore { todos: Mutex::new(Vec::new()), next_id: Mutex::new(1), } } /// Adds a new todo and returns a clone of it (so the handler can render it). fn add(&self, title: String) -> Todo { let mut todos = self.todos.lock().unwrap(); let mut next_id = self.next_id.lock().unwrap(); let todo = Todo { id: *next_id, title, completed: false, }; *next_id += 1; todos.push(Todo { id: todo.id, title: todo.title.clone(), completed: todo.completed, }); todo } /// Toggles the completed status of a todo by ID. Returns the updated todo if found. fn toggle(&self, id: u32) -> Option { let mut todos = self.todos.lock().unwrap(); todos.iter_mut().find(|t| t.id == id).map(|todo| { todo.completed = !todo.completed; Todo { id: todo.id, title: todo.title.clone(), completed: todo.completed, } }) } /// Deletes a todo by ID. Returns true if something was removed. fn delete(&self, id: u32) -> bool { let mut todos = self.todos.lock().unwrap(); let len_before = todos.len(); todos.retain(|t| t.id != id); todos.len() < len_before } /// Returns a snapshot (cloned Vec) of all todos for rendering. fn list(&self) -> Vec { self.todos.lock().unwrap().clone() } } /// Takes a slice of todos and generates the full HTML String response, /// including a new-todo input form and HTMX triggers for toggling/deleting. fn render_todo_page(todos: &[Todo]) -> String { html! { (DOCTYPE) html lang="en" { head { meta charset="UTF-8"; meta name="viewport" content="width=device-width, initial-scale=1.0"; title { "Rust + HTMX + Tailwind Todos" } script src="https://cdn.tailwindcss.com" {} script src="https://unpkg.com/htmx.org@1.9.10" {} } body class="bg-slate-50 text-slate-800 min-h-screen flex items-center justify-center" { div class="max-w-md w-full bg-white rounded-xl shadow-md p-6 m-4" { h1 class="text-2xl font-bold text-slate-900 mb-6 text-center" { "My Todo List" } // 1. Input Form to create a new todo form hx-post="/todos" hx-target="#todo-list" hx-swap="beforeend" class="flex gap-2 mb-6" { input type="text" name="title" placeholder="Add a new todo..." required class="flex-1 px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"; button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors" { "Add" } } // 2. Todo List Container div id="todo-list" class="space-y-3" { @for todo in todos { (render_todo_item(todo)) } } } } } }.into_string() } /// Renders an individual todo item (useful to reuse when returning a single updated item via HTMX) fn render_todo_item(todo: &Todo) -> maud::Markup { let text_style = if todo.completed { "line-through text-slate-400" } else { "text-slate-700" }; html! { div class="flex items-center justify-between p-3 bg-slate-100 rounded-lg border border-slate-200 todo" { div class="flex items-center space-x-3" { input type="checkbox" checked[todo.completed] // HTMX attribute to toggle completion on the server hx-patch=(format!("/todos/{}/toggle", todo.id)) hx-target="closest div.todo" hx-swap="outerHTML" class="w-4 h-4 text-blue-600 rounded border-slate-300 focus:ring-blue-500 cursor-pointer"; span class=(text_style) { (todo.title) } } button class="text-red-500 hover:text-red-700 text-sm font-medium transition-colors" hx-delete=(format!("/todos/{}", todo.id)) hx-target="closest div.flex" hx-swap="outerHTML" { "Delete" } } } } #[get("/")] fn index(store: &State) -> RawHtml { let todos = store.list(); RawHtml(render_todo_page(&todos)) } #[derive(FromForm)] struct CreateRequest { title: String, } #[post("/todos", data = "")] fn create_todo(request: Form, store: &State) -> RawHtml { let todo = store.add(request.title.clone()); RawHtml(render_todo_item(&todo).into_string()) } #[patch("/todos//toggle")] fn toggle_todo(id: u32, store: &State) -> RawHtml { match store.toggle(id) { Some(todo) => RawHtml(render_todo_item(&todo).into_string()), None => RawHtml(String::new()), } } #[delete("/todos/")] fn delete_todo(id: u32, store: &State) -> RawHtml { store.delete(id); RawHtml(String::new()) } #[launch] fn rocket() -> _ { let args: Vec = env::args().collect(); let program = &args[0]; println!("Program: {}", program); if let Some(command) = args.get(1) { println!("Command: {}", command); match command.as_str() { "add" => { println!("command"); // parse -m flag value }, _ => todo!() } } rocket::build() .manage(TodoStore::new()) .mount("/", routes![index, create_todo, delete_todo, toggle_todo]) }