tune_cli/
lib.rs

1mod dto;
2mod error;
3mod est;
4mod live;
5mod midi;
6mod mos;
7mod mts;
8mod portable;
9mod scala;
10mod scale;
11
12use std::{
13    fmt::{self, Display},
14    fs::File,
15    io::{self, ErrorKind, Write},
16    path::PathBuf,
17};
18
19use clap::Parser;
20use error::ResultExt;
21use est::EstOptions;
22use futures::executor;
23use io::Read;
24use live::LiveOptions;
25use mos::MosCommand;
26use mts::MtsOptions;
27use scala::{KbmCommand, SclOptions};
28use scale::{DiffOptions, DumpOptions, ScaleCommand};
29
30#[doc(hidden)]
31pub mod shared;
32
33#[derive(Parser)]
34#[command(version)]
35struct MainOptions {
36    /// Write output to a file instead of stdout
37    #[arg(long = "of")]
38    output_file: Option<PathBuf>,
39
40    #[command(subcommand)]
41    command: MainCommand,
42}
43
44#[derive(Parser)]
45enum MainCommand {
46    /// Create a scale file
47    #[command(name = "scl")]
48    Scl(SclOptions),
49
50    /// Create a keyboard mapping file
51    #[command(subcommand, name = "kbm")]
52    Kbm(KbmCommand),
53
54    /// Analyze equal-step tunings
55    #[command(name = "est")]
56    Est(EstOptions),
57
58    /// Find MOS scales from generators or vice versa
59    #[command(subcommand, name = "mos")]
60    Mos(MosCommand),
61
62    /// Print a scale to stdout
63    #[command(subcommand, name = "scale")]
64    Scale(ScaleCommand),
65
66    /// Display details of a scale
67    #[command(name = "dump")]
68    Dump(DumpOptions),
69
70    /// Display differences between a source scale and a target scale
71    #[command(name = "diff")]
72    Diff(DiffOptions),
73
74    /// Print MIDI Tuning Standard messages and/or send them to MIDI devices
75    #[command(name = "mts")]
76    Mts(MtsOptions),
77
78    /// Enable synthesizers with limited tuning support to be played in any tuning.
79    /// This is achieved by reading MIDI data from a sequencer/keyboard and sending modified MIDI data to a synthesizer.
80    /// The sequencer/keyboard and synthesizer can be the same device. In this case, remember to disable local keyboard playback.
81    #[command(name = "live")]
82    Live(LiveOptions),
83
84    /// List MIDI devices
85    #[command(name = "devices")]
86    Devices,
87}
88
89impl MainOptions {
90    async fn run(self) -> Result<(), CliError> {
91        let output: Box<dyn Write> = match self.output_file {
92            Some(output_file) => Box::new(File::create(output_file)?),
93            None => Box::new(io::stdout()),
94        };
95
96        let mut app = App {
97            input: Box::new(io::stdin()),
98            output,
99            error: Box::new(io::stderr()),
100        };
101
102        self.command.run(&mut app).await
103    }
104}
105
106impl MainCommand {
107    async fn run(self, app: &mut App<'_>) -> CliResult {
108        match self {
109            MainCommand::Scl(options) => options.run(app),
110            MainCommand::Kbm(options) => options.run(app),
111            MainCommand::Est(options) => options.run(app),
112            MainCommand::Mos(options) => options.run(app),
113            MainCommand::Scale(options) => options.run(app),
114            MainCommand::Dump(options) => options.run(app),
115            MainCommand::Diff(options) => options.run(app),
116            MainCommand::Mts(options) => options.run(app),
117            MainCommand::Live(options) => options.run(app).await,
118            MainCommand::Devices => midi::print_midi_devices(&mut app.output, "tune-cli")
119                .handle_error("Could not print MIDI devices"),
120        }
121    }
122}
123
124pub fn run_in_shell_env() {
125    let options = match MainOptions::try_parse() {
126        Err(err) => {
127            if err.use_stderr() {
128                eprintln!("{err}")
129            } else {
130                println!("{err}");
131            };
132            return;
133        }
134        Ok(options) => options,
135    };
136
137    match executor::block_on(options.run()) {
138        Ok(()) => {}
139        // The BrokenPipe case occurs when stdout tries to communicate with a process that has already terminated.
140        // Since tune is an idempotent tool with repeatable results, it is okay to ignore this error and terminate successfully.
141        Err(CliError::IoError(err)) if err.kind() == ErrorKind::BrokenPipe => {}
142        Err(err) => eprintln!("{err}"),
143    }
144}
145
146pub fn run_in_wasm_env(
147    args: impl IntoIterator<Item = String>,
148    input: impl Read,
149    output: impl Write,
150    error: impl Write,
151) {
152    let mut app = App {
153        input: Box::new(input),
154        output: Box::new(output),
155        error: Box::new(error),
156    };
157
158    let command = match MainCommand::try_parse_from(args) {
159        Err(err) => {
160            if err.use_stderr() {
161                app.errln(err).unwrap()
162            } else {
163                app.writeln(err).unwrap()
164            };
165            return;
166        }
167        Ok(command) => command,
168    };
169
170    match executor::block_on(command.run(&mut app)) {
171        Ok(()) => {}
172        Err(err) => app.errln(err).unwrap(),
173    }
174}
175
176struct App<'a> {
177    input: Box<dyn 'a + Read>,
178    output: Box<dyn 'a + Write>,
179    error: Box<dyn 'a + Write>,
180}
181
182impl App<'_> {
183    pub fn write(&mut self, message: impl Display) -> io::Result<()> {
184        write!(self.output, "{message}")
185    }
186
187    pub fn writeln(&mut self, message: impl Display) -> io::Result<()> {
188        writeln!(self.output, "{message}")
189    }
190
191    pub fn errln(&mut self, message: impl Display) -> io::Result<()> {
192        writeln!(self.error, "{message}")
193    }
194
195    pub fn read(&mut self) -> &mut dyn Read {
196        &mut self.input
197    }
198}
199
200pub type CliResult<T = ()> = Result<T, CliError>;
201
202pub enum CliError {
203    CommandError(String),
204    IoError(io::Error),
205}
206
207impl Display for CliError {
208    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209        match self {
210            CliError::CommandError(err) => write!(f, "error: {err}"),
211            CliError::IoError(err) => write!(f, "I/O error: {err}"),
212        }
213    }
214}
215
216impl From<String> for CliError {
217    fn from(v: String) -> Self {
218        CliError::CommandError(v)
219    }
220}
221
222impl From<io::Error> for CliError {
223    fn from(v: io::Error) -> Self {
224        CliError::IoError(v)
225    }
226}