moont-live 1.0.0

Real-time CM-32L MIDI sink using ALSA
// Copyright (C) 2021-2026 Geoff Hill <geoff@geoffhill.org>
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 2.1 of the License, or (at
// your option) any later version. Read COPYING.LESSER.txt for details.

use moont::ControlCommand;
use std::sync::mpsc;

const HISTORY_FILE: &str = ".moont-history";

pub enum ReplCommand {
    Controls(Vec<ControlCommand>),
    Quit,
}

enum ParsedCommand {
    Controls(Vec<ControlCommand>),
    Help,
    Quit,
}

fn print_help() {
    eprintln!("Commands:");
    eprintln!("  set part <1-9> [program <0-127>] [volume <0-100>]");
    eprintln!("  set master volume <0-100>");
    eprintln!("  set reverb [mode <0-3>] [time <0-7>] [level <0-7>]");
    eprintln!("  reset");
    eprintln!("  help");
    eprintln!("  quit");
}

fn parse_u8(token: Option<&str>, what: &str) -> Result<u8, String> {
    let Some(s) = token else {
        return Err(format!("missing {what}"));
    };
    s.parse::<u8>().map_err(|_| format!("invalid {what}: {s}"))
}

fn parse_repl_command(line: &str) -> Result<ParsedCommand, String> {
    let mut t = line.split_whitespace();
    let Some(cmd) = t.next() else {
        return Err("empty command".into());
    };

    match cmd {
        "help" => Ok(ParsedCommand::Help),
        "quit" | "exit" => Ok(ParsedCommand::Quit),
        "reset" => Ok(ParsedCommand::Controls(vec![ControlCommand::Reset])),
        "set" => {
            let Some(scope) = t.next() else {
                return Err("missing set target".into());
            };
            match scope {
                "part" => {
                    let part_1 = parse_u8(t.next(), "part index")?;
                    if !(1..=9).contains(&part_1) {
                        return Err(format!(
                            "part out of range (expected 1-9): {part_1}"
                        ));
                    }
                    let part = part_1 - 1;
                    let mut program = None;
                    let mut volume = None;

                    while let Some(field) = t.next() {
                        match field {
                            "program" => {
                                if program.is_some() {
                                    return Err(
                                        "duplicate part field: program".into(),
                                    );
                                }
                                if part > 7 {
                                    return Err(format!(
                                        "program is only valid on melodic parts 1-8: {part_1}"
                                    ));
                                }
                                let value = parse_u8(t.next(), "program")?;
                                if value > 127 {
                                    return Err(format!(
                                        "program out of range (expected 0-127): {value}"
                                    ));
                                }
                                program = Some(value);
                            }
                            "volume" => {
                                if volume.is_some() {
                                    return Err(
                                        "duplicate part field: volume".into()
                                    );
                                }
                                let value = parse_u8(t.next(), "volume")?;
                                if value > 100 {
                                    return Err(format!(
                                        "volume out of range (expected 0-100): {value}"
                                    ));
                                }
                                volume = Some(value);
                            }
                            _ => {
                                return Err(format!(
                                    "unknown part field: {field}"
                                ));
                            }
                        }
                    }

                    let mut controls = Vec::new();
                    if let Some(program) = program {
                        controls.push(ControlCommand::SetPartProgram {
                            part,
                            program,
                        });
                    }
                    if let Some(volume) = volume {
                        controls.push(ControlCommand::SetPartVolume {
                            part,
                            volume,
                        });
                    }
                    if controls.is_empty() {
                        return Err(
                            "set part requires at least one field".into()
                        );
                    }

                    Ok(ParsedCommand::Controls(controls))
                }
                "master" => {
                    let Some(field) = t.next() else {
                        return Err("missing master field".into());
                    };
                    if field != "volume" {
                        return Err(format!("unknown master field: {field}"));
                    }
                    let volume = parse_u8(t.next(), "volume")?;
                    if volume > 100 {
                        return Err(format!(
                            "volume out of range (expected 0-100): {volume}"
                        ));
                    }
                    Ok(ParsedCommand::Controls(vec![
                        ControlCommand::SetMasterVolume { volume },
                    ]))
                }
                "reverb" => {
                    let mut mode = None;
                    let mut time = None;
                    let mut level = None;

                    while let Some(field) = t.next() {
                        match field {
                            "mode" => {
                                if mode.is_some() {
                                    return Err(
                                        "duplicate reverb field: mode".into()
                                    );
                                }
                                let value = parse_u8(t.next(), "reverb mode")?;
                                if value > 3 {
                                    return Err(format!(
                                        "reverb mode out of range: {value}"
                                    ));
                                }
                                mode = Some(value);
                            }
                            "time" => {
                                if time.is_some() {
                                    return Err(
                                        "duplicate reverb field: time".into()
                                    );
                                }
                                let value = parse_u8(t.next(), "reverb time")?;
                                if value > 7 {
                                    return Err(format!(
                                        "reverb time out of range: {value}"
                                    ));
                                }
                                time = Some(value);
                            }
                            "level" => {
                                if level.is_some() {
                                    return Err(
                                        "duplicate reverb field: level".into(),
                                    );
                                }
                                let value = parse_u8(t.next(), "reverb level")?;
                                if value > 7 {
                                    return Err(format!(
                                        "reverb level out of range: {value}"
                                    ));
                                }
                                level = Some(value);
                            }
                            _ => {
                                return Err(format!(
                                    "unknown reverb field: {field}"
                                ));
                            }
                        }
                    }

                    let mut controls = Vec::new();
                    if let Some(mode) = mode {
                        controls.push(ControlCommand::SetReverbMode { mode });
                    }
                    if let Some(time) = time {
                        controls.push(ControlCommand::SetReverbTime { time });
                    }
                    if let Some(level) = level {
                        controls.push(ControlCommand::SetReverbLevel { level });
                    }
                    if controls.is_empty() {
                        return Err(
                            "set reverb requires at least one field".into()
                        );
                    }

                    Ok(ParsedCommand::Controls(controls))
                }
                _ => Err(format!("unknown set target: {scope}")),
            }
        }
        _ => Err(format!("unknown command: {cmd}")),
    }
}

pub fn spawn_repl(
    tx: mpsc::SyncSender<ReplCommand>,
) -> std::thread::JoinHandle<()> {
    std::thread::spawn(move || {
        let mut rl = match rustyline::DefaultEditor::new() {
            Ok(rl) => rl,
            Err(e) => {
                eprintln!("Failed to initialize readline: {e}");
                let _ = tx.send(ReplCommand::Quit);
                return;
            }
        };
        let _ = rl.load_history(HISTORY_FILE);
        print_help();
        loop {
            match rl.readline("moont> ") {
                Ok(line) => {
                    let line = line.trim();
                    if line.is_empty() {
                        continue;
                    }
                    let _ = rl.add_history_entry(line);
                    match parse_repl_command(line) {
                        Ok(ParsedCommand::Controls(controls)) => {
                            if tx.send(ReplCommand::Controls(controls)).is_err()
                            {
                                break;
                            }
                        }
                        Ok(ParsedCommand::Help) => {
                            print_help();
                        }
                        Ok(ParsedCommand::Quit) => {
                            let _ = tx.send(ReplCommand::Quit);
                            break;
                        }
                        Err(e) => {
                            eprintln!("{e}");
                            eprintln!("Type `help` for commands.");
                        }
                    }
                }
                Err(rustyline::error::ReadlineError::Interrupted)
                | Err(rustyline::error::ReadlineError::Eof) => {
                    let _ = tx.send(ReplCommand::Quit);
                    break;
                }
                Err(e) => {
                    eprintln!("Readline error: {e}");
                    let _ = tx.send(ReplCommand::Quit);
                    break;
                }
            }
        }
        if let Err(e) = rl.save_history(HISTORY_FILE) {
            eprintln!("Failed to save history: {e}");
        }
    })
}