midi-io 0.2.1

Stream and send strictly-typed MIDI messages
Documentation
use std::fs;
use std::io::Read;
use std::io::Write;
use std::net::TcpListener;
use std::net::TcpStream;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;

use wasm_bindgen_cli_support::Bindgen;

fn main() {
    if let Err(e) = run() {
        eprintln!("web-synth: {e}");
        std::process::exit(1);
    }
}

fn run() -> Result<(), String> {
    let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples/web_synth_app");
    build_wasm(&crate_dir)?;

    let pkg_dir = crate_dir.join("pkg");
    fs::create_dir_all(&pkg_dir).map_err(|e| e.to_string())?;
    let wasm = crate_dir.join("target/wasm32-unknown-unknown/release/web_synth_app.wasm");

    Bindgen::new()
        .input_path(&wasm)
        .web(true)
        .map_err(|e| e.to_string())?
        .generate(&pkg_dir)
        .map_err(|e| e.to_string())?;

    fs::copy(crate_dir.join("index.html"), pkg_dir.join("index.html"))
        .map_err(|e| e.to_string())?;

    serve(&pkg_dir)
}

fn build_wasm(crate_dir: &Path) -> Result<(), String> {
    let status = Command::new("cargo")
        .current_dir(crate_dir)
        .env_remove("RUSTFLAGS")
        .env_remove("CARGO_ENCODED_RUSTFLAGS")
        .args(["build", "--release", "--target", "wasm32-unknown-unknown"])
        .status()
        .map_err(|e| format!("failed to launch cargo: {e}"))?;
    if !status.success() {
        return Err("wasm build failed (is the wasm32-unknown-unknown target installed?)".into());
    }
    Ok(())
}

fn serve(dir: &Path) -> Result<(), String> {
    let listener = TcpListener::bind("127.0.0.1:0").map_err(|e| e.to_string())?;
    let port = listener.local_addr().map_err(|e| e.to_string())?.port();
    let url = format!("http://localhost:{port}");
    println!("web-synth serving at {url}");
    open_browser(&url);

    for stream in listener.incoming() {
        match stream {
            Ok(stream) => handle(stream, dir),
            Err(e) => eprintln!("web-synth: connection error: {e}"),
        }
    }
    Ok(())
}

fn handle(mut stream: TcpStream, dir: &Path) {
    let mut buffer = [0u8; 1024];
    let read = match stream.read(&mut buffer) {
        Ok(n) => n,
        Err(_) => return,
    };
    let request = String::from_utf8_lossy(&buffer[..read]);
    let path = request
        .lines()
        .next()
        .and_then(|line| line.split_whitespace().nth(1))
        .unwrap_or("/");

    let relative = match path {
        "/" => "index.html",
        other => other.trim_start_matches('/'),
    };

    if relative.contains("..") {
        let _ = respond(&mut stream, "403 Forbidden", "text/plain", b"forbidden");
        return;
    }

    match fs::read(dir.join(relative)) {
        Ok(body) => {
            let _ = respond(&mut stream, "200 OK", content_type(relative), &body);
        }
        Err(_) => {
            let _ = respond(&mut stream, "404 Not Found", "text/plain", b"not found");
        }
    }
}

fn respond(stream: &mut TcpStream, status: &str, mime: &str, body: &[u8]) -> std::io::Result<()> {
    let header = format!(
        "HTTP/1.1 {status}\r\nContent-Type: {mime}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
        body.len()
    );
    stream.write_all(header.as_bytes())?;
    stream.write_all(body)?;
    stream.flush()
}

fn content_type(path: &str) -> &'static str {
    if path.ends_with(".html") {
        "text/html; charset=utf-8"
    } else if path.ends_with(".js") {
        "text/javascript"
    } else if path.ends_with(".wasm") {
        "application/wasm"
    } else {
        "application/octet-stream"
    }
}

fn open_browser(url: &str) {
    let launcher = if cfg!(target_os = "macos") {
        Some(("open", vec![url]))
    } else if cfg!(target_os = "windows") {
        Some(("cmd", vec!["/C", "start", url]))
    } else {
        Some(("xdg-open", vec![url]))
    };
    if let Some((program, args)) = launcher {
        let _ = Command::new(program).args(args).spawn();
    }
}