1use std::path::{Path, PathBuf};
3use tokio::io::{AsyncReadExt, AsyncWriteExt};
4use tokio::net::{TcpListener, TcpStream};
5use tokio::sync::broadcast;
7const RELOAD_SCRIPT: &str =
8 r#"<script>new EventSource("/_events").onmessage=()=>location.reload()</script>"#;
10pub async fn run(port: u16, html_path: PathBuf, reload: broadcast::Sender<()>) {
11 let listener = match TcpListener::bind(("127.0.0.1", port)).await {
14 eprintln!("error: failed to bind to port {}: {}", port, e);
20 if let Ok((stream, _)) = listener.accept().await {
21 let path = html_path.clone();
22 let rx = reload.subscribe();
23 tokio::spawn(async move {
24 let _ = handle(stream, path, rx).await;
31 mut stream: TcpStream,
33 reload_rx: broadcast::Receiver<()>,
34) -> std::io::Result<()> {
35 let mut buf = [0u8; 4096];
36 let n = stream.read(&mut buf).await?;
41 let request = String::from_utf8_lossy(&buf[..n]);
45 .and_then(|line| line.split_whitespace().nth(1))
48 if path == "/_events" {
49 handle_sse(stream, reload_rx).await
51 handle_file(stream, &html_path, path).await
56 mut stream: TcpStream,
57 mut rx: broadcast::Receiver<()>,
58) -> std::io::Result<()> {
59 let headers = "HTTP/1.1 200 OK\r\n\
60 Content-Type: text/event-stream\r\n\
61 Cache-Control: no-cache\r\n\
62 Connection: keep-alive\r\n\r\n";
63 stream.write_all(headers.as_bytes()).await?;
66 match rx.recv().await {
68 if stream.write_all(b"data: reload\n\n").await.is_err() {
72 Err(broadcast::error::RecvError::Lagged(_)) => continue,
73 Err(broadcast::error::RecvError::Closed) => break,
81 mut stream: TcpStream,
84) -> std::io::Result<()> {
85 if request_path.contains("..") {
87 .write_all(b"HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n")
92 let file_path = if request_path == "/" {
93 html_path.to_path_buf()
95 let dir = html_path.parent().unwrap_or(Path::new("."));
96 dir.join(&request_path[1..])
99 let body = match std::fs::read(&file_path) {
101 if file_path.extension().map_or(false, |e| e == "html") {
102 inject_reload_script(&content)
109 .write_all(b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n")
115 let content_type = content_type_for(&file_path);
116 let header = format!(
117 "HTTP/1.1 200 OK\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nCache-Control: no-cache\r\n\r\n",
120 stream.write_all(header.as_bytes()).await?;
121 stream.write_all(&body).await?;
126fn inject_reload_script(content: &[u8]) -> Vec<u8> {
127 let html = String::from_utf8_lossy(content);
128 match html.rfind("</body>") {
130 let mut result = String::with_capacity(html.len() + RELOAD_SCRIPT.len() + 1);
131 result.push_str(&html[..pos]);
132 result.push_str(RELOAD_SCRIPT);
134 result.push_str(&html[pos..]);
138 let mut result = content.to_vec();
139 result.extend_from_slice(RELOAD_SCRIPT.as_bytes());
145fn content_type_for(path: &Path) -> &'static str {
146 match path.extension().and_then(|e| e.to_str()) {
147 Some("html") => "text/html; charset=utf-8",
148 Some("css") => "text/css; charset=utf-8",
149 Some("js") => "application/javascript",
150 Some("json") => "application/json",
151 Some("png") => "image/png",
152 Some("jpg" | "jpeg") => "image/jpeg",
153 Some("gif") => "image/gif",
154 Some("svg") => "image/svg+xml",
155 Some("ico") => "image/x-icon",
156 Some("woff") => "font/woff",
157 Some("woff2") => "font/woff2",
158 _ => "application/octet-stream",
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list