Skip to main content

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