diff --git a/Cargo.lock b/Cargo.lock index 038b641..3269974 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,6 +62,7 @@ version = "0.1.0" dependencies = [ "anyhow", "atty", + "clap", "serde", "serde_json", "shellwords", diff --git a/Cargo.toml b/Cargo.toml index 75072cb..0e77e69 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ name = "germ" [dependencies] anyhow = "1.0" atty = "0.2" +clap = "2" serde = { version = "1.0", features = ["derive"] } serde_json = "1" shellwords = "1.1.0" diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..8763f4d --- /dev/null +++ b/src/app.rs @@ -0,0 +1,478 @@ +// Copyright (C) 2021 Christopher R. Field +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use crate::asciicast::Asciicast; +use crate::sequence::{Command, Sequence, Timings, DEFAULT_PROMPT}; +use crate::termsheets; +use anyhow::Result; +use atty::Stream; +use clap::value_t; +use std::fs::File; +use std::io; +use std::io::{BufRead, BufReader, Read, Write}; +use std::path::PathBuf; +use std::process; +use structopt::clap::{self, ArgMatches}; +use structopt::StructOpt; +use strum::{Display, EnumString, EnumVariantNames, VariantNames}; + +pub const DEFAULT_INTERACTIVE_PROMPT: &str = ">>> "; + +#[derive(Display, Debug, EnumString, EnumVariantNames)] +#[strum(serialize_all = "lowercase")] +enum InputFormats { + Germ, + TermSheets, +} + +impl Default for InputFormats { + fn default() -> Self { + Self::Germ + } +} + +#[derive(Display, Debug, EnumString, EnumVariantNames)] +#[strum(serialize_all = "lowercase")] +enum OutputFormats { + Germ, + TermSheets, + Asciicast, +} + +impl Default for OutputFormats { + fn default() -> Self { + Self::Asciicast + } +} + +#[derive(Debug, StructOpt)] +#[structopt(settings(&[ + clap::AppSettings::NoBinaryName, + clap::AppSettings::DisableHelpFlags, + clap::AppSettings::DisableVersion, + clap::AppSettings::NextLineHelp]), + usage("[FLAGS] [OPTIONS] [INPUT] [OUTPUTS...]") +)] +struct Interactive { + #[structopt(flatten)] + cli: Cli, + + /// Prints the current sequence. + /// + /// The format is determined by the last output format used. + #[structopt(long)] + print: bool, + + /// Prints help information. + #[structopt(short = "h")] + short_help: bool, + + /// Prints more help information. + #[structopt(long = "help")] + long_help: bool, + + /// Prints version information. + #[structopt(short = "V", long = "version")] + version: bool, +} + +#[derive(Debug, StructOpt)] +#[structopt(about = "Generate terminal session recording files without rehearsing and recording")] +pub struct Cli { + #[structopt(flatten)] + timings: Timings, + + #[structopt(flatten)] + asciicast: Asciicast, + + /// A comment about the command. + /// + /// A line will be "printed" in the terminal session above the prompt and input. + #[structopt(short, long)] + comment: Option, + + /// The prompt to display before the command. + #[structopt(short = "p", long, default_value = DEFAULT_PROMPT, env = "GERM_PROMPT")] + prompt: String, + + /// The prompt displayed in interactive mode. + #[structopt(short ="P", long, default_value = DEFAULT_INTERACTIVE_PROMPT, env = "GERM_INTERACTIVE_PROMPT")] + interactive_prompt: String, + + /// Use the Germ JSON format for the output. + /// + /// This is equivalent to '-O,--output-format germ'. + #[structopt(short = "G")] + use_germ_format: bool, + + /// Prints the license information. + /// + /// This is as recommended by the GPL-3.0 license. + #[structopt(long)] + license: bool, + + /// Prints the warranty information. + /// + /// This is as recommended by the GPL-3.0 license. + #[structopt(long)] + warranty: bool, + + /// The format of the input. + #[structopt( + short = "I", + long, + possible_values = InputFormats::VARIANTS, + case_insensitive = true, + default_value, + value_name = "format", + env = "GERM_INPUT_FORMAT" + )] + input_format: InputFormats, + + /// Input file in the commands JSON format. + /// + /// If not present, then stdin if it is piped or redirected. + #[structopt(short = "i", long = "input", value_name("file"), parse(from_os_str))] + input_file: Option, + + /// The format for the output. + #[structopt( + short = "O", + long, + possible_values = OutputFormats::VARIANTS, + case_insensitive = true, + default_value, + default_value_if("use-germ-format", None, "germ"), + value_name = "format", + env = "GERM_OUTPUT_FORMAT" + )] + output_format: OutputFormats, + + /// Output file, stdout if not present. + /// + /// This is useful if using the application in interactive mode. + #[structopt(short = "o", long = "output", value_name("file"), parse(from_os_str))] + output_file: Option, + + /// The command entered at the prompt. + /// + /// If not present and the -i,--input option is not used, then the + /// application enters an interactive mode where commands are manually + /// entered one at a time within the terminal. If present, then the command + /// is appended to the sequence of commands from any input file or stdin. + /// + /// Note, if present without any output, then the input will be executed + /// within a child shell process and the execution output will be used. + input: Option, + + /// Output from the command. + /// + /// If no output is provided, then the input will be execute within a child + /// shell process and execution output will be used. + outputs: Vec, +} + +impl Cli { + pub fn execute(mut self) -> Result<()> { + if self.license { + print_license(); + return Ok(()); + } + if self.warranty { + print_warranty(); + return Ok(()); + } + let mut sequence = self.read()?; + self.append(&mut sequence)?; + self.write(sequence) + } + + fn read(&self) -> Result { + if let Some(input_file) = &self.input_file { + self.read_from(BufReader::new(File::open(input_file)?)) + } else if atty::is(Stream::Stdin) { + Ok(Sequence::from(self.timings)) + } else { + let stdin = io::stdin(); + self.read_from(stdin) + } + } + + fn read_from(&self, r: R) -> Result { + match self.input_format { + InputFormats::Germ => serde_json::from_reader(r).map_err(anyhow::Error::from), + InputFormats::TermSheets => { + let termsheets: Vec = serde_json::from_reader(r)?; + let mut sequence = Sequence::from(self.timings); + sequence.append( + &mut termsheets + .into_iter() + .map(|c| { + let mut cmd = Command::from(c); + cmd.set_prompt(&self.prompt); + cmd + }) + .collect(), + ); + Ok(sequence) + } + } + } + + fn append(&mut self, sequence: &mut Sequence) -> Result<()> { + if let Some(input) = self.input.as_ref() { + self.append_arguments(sequence, input) + } else if self.input_file.is_none() && atty::is(Stream::Stdin) { + self.append_interactively(sequence) + } else { + Ok(()) + } + } + + fn append_arguments(&self, sequence: &mut Sequence, input: &str) -> Result<()> { + let mut outputs = if self.outputs.is_empty() { + let output = self.execute_cmd(input)?; + vec![std::str::from_utf8(&output.stdout)?.to_owned()] + } else { + self.outputs.clone() + }; + sequence.add({ + let mut cmd = Command::from(input); + cmd.set_comment(self.comment.as_deref()); + cmd.set_prompt(&self.prompt); + cmd.append(&mut outputs); + cmd + }); + Ok(()) + } + + fn append_interactively(&mut self, sequence: &mut Sequence) -> Result<()> { + print_interactive_notice(); + println!(); + let mut stdout = io::stdout(); + stdout.write_all(self.interactive_prompt.as_bytes())?; + stdout.flush()?; + for line in io::stdin().lock().lines() { + let words = shellwords::split(&line.expect("stdin line"))?; + let mut app = Interactive::clap(); + match app.get_matches_from_safe_borrow(words) { + Ok(matches) => { + if matches.is_present("short-help") { + app.write_help(&mut stdout)?; + stdout.write_all(b"\n")?; + } else if matches.is_present("long-help") { + app.write_long_help(&mut stdout)?; + stdout.write_all(b"\n")?; + } else if matches.is_present("short-version") { + app.write_version(&mut stdout)?; + stdout.write_all(b"\n")?; + } else if matches.is_present("long-version") { + app.write_long_version(&mut stdout)?; + stdout.write_all(b"\n")?; + } else if matches.is_present("license") { + print_license(); + } else if matches.is_present("warranty") { + print_warranty(); + } else if matches.is_present("print") { + self.write_to(&mut stdout, &sequence)?; + if !matches!(self.output_format, OutputFormats::Asciicast) { + stdout.write_all(b"\n")?; + } + } else { + self.update_from(&matches); + if let Some(input_file) = matches.value_of("input-file").map(PathBuf::from) + { + sequence.append_from( + self.read_from(BufReader::new(File::open(input_file)?))?, + ); + } + if let Some(input) = matches.value_of("input") { + let mut outputs = if matches.is_present("outputs") { + matches + .values_of("outputs") + .unwrap() + .map(String::from) + .collect() + } else { + let output = self.execute_cmd(&input)?; + stdout.write_all(&output.stdout)?; + vec![std::str::from_utf8(&output.stdout)?.to_owned()] + }; + sequence.add({ + let mut cmd = Command::from(input); + cmd.set_comment( + matches.value_of("comment").map(String::from).as_deref(), + ); + cmd.set_prompt(&self.prompt); + cmd.append(&mut outputs); + cmd + }); + } + } + } + Err(err) => eprintln!("{}", err), + } + stdout.write_all(self.interactive_prompt.as_bytes())?; + stdout.flush()?; + } + stdout.write_all(b"\n")?; + stdout.flush()?; + Ok(()) + } + + fn write(&mut self, sequence: Sequence) -> Result<()> { + let writer: Box = if let Some(output_file) = &self.output_file { + Box::new(File::create(output_file)?) + } else { + Box::new(io::stdout()) + }; + self.write_to(writer, &sequence) + } + + fn write_to(&mut self, mut writer: W, sequence: &Sequence) -> Result<()> { + match self.output_format { + OutputFormats::Germ => { + serde_json::to_writer(&mut writer, &sequence)?; + } + OutputFormats::TermSheets => { + let termsheets: Vec = sequence.into(); + serde_json::to_writer(&mut writer, &termsheets)?; + } + OutputFormats::Asciicast => { + self.asciicast + .append_from(&sequence) + .write_to(&mut writer)?; + } + } + Ok(()) + } + + fn update_from(&mut self, matches: &ArgMatches) { + if matches.occurrences_of("interactive-prompt") != 0 { + self.interactive_prompt = value_t!(matches, "interactive-prompt", String).unwrap(); + } + if matches.occurrences_of("begin-delay") != 0 { + self.timings.begin = value_t!(matches, "begin-delay", f64).unwrap(); + } + if matches.occurrences_of("delay-type-start") != 0 { + self.timings.type_start = value_t!(matches, "delay-type-start", usize).unwrap(); + } + if matches.occurrences_of("delay-type-char") != 0 { + self.timings.type_char = value_t!(matches, "delay-type-char", usize).unwrap(); + } + if matches.occurrences_of("delay-type-submit") != 0 { + self.timings.type_submit = value_t!(matches, "delay-type-start", usize).unwrap(); + } + if matches.occurrences_of("delay-output-line") != 0 { + self.timings.output_line = value_t!(matches, "delay-output-line", usize).unwrap(); + } + if matches.occurrences_of("end-delay") != 0 { + self.timings.end = value_t!(matches, "end-delay", f64).unwrap(); + } + if matches.occurrences_of("title") != 0 { + self.asciicast.header.title = value_t!(matches, "title", String).ok(); + } + if matches.occurrences_of("width") != 0 { + self.asciicast.header.width = value_t!(matches, "width", usize).unwrap(); + } + if matches.occurrences_of("height") != 0 { + self.asciicast.header.height = value_t!(matches, "height", usize).unwrap(); + } + if matches.occurrences_of("input-format") != 0 { + self.input_format = value_t!(matches, "input-format", InputFormats).unwrap(); + } + if matches.occurrences_of("output-format") != 0 { + self.output_format = value_t!(matches, "output-format", OutputFormats).unwrap(); + } + if matches.occurrences_of("output-file") != 0 { + self.output_file = value_t!(matches, "output-file", PathBuf).ok(); + } + if matches.occurrences_of("prompt") != 0 { + self.prompt = value_t!(matches, "prompt", String).unwrap(); + } + if matches.occurrences_of("speed") != 0 { + self.timings.speed = value_t!(matches, "speed", f64).unwrap(); + } + if matches.occurrences_of("shell") != 0 { + self.asciicast.header.env.shell = value_t!(matches, "shell", String).unwrap(); + } + if matches.occurrences_of("term") != 0 { + self.asciicast.header.env.term = value_t!(matches, "shell", String).unwrap(); + } + if matches.occurrences_of("stdin") != 0 { + self.asciicast.stdin = true; + } + if matches.occurrences_of("use-germ-format") != 0 { + self.use_germ_format = true; + } + } + + fn execute_cmd(&self, input: &str) -> Result { + process::Command::new(&self.asciicast.header.env.shell) + .args(&[ + &format!("{}", self.asciicast.header.env.execute_string_flag), + input, + ]) + .output() + .map_err(anyhow::Error::from) + } +} + +fn print_interactive_notice() { + println!( + r#"Copyright (C) 2021 Christopher R. Field +This program comes with ABSOLUTELY NO WARRANTY; for details use the `--warranty` +flag. This is free software, and you are welcome to redistirbute it under +certain conditions; use the `--license` flag for details. + +You have entered interactive mode. The prompt has similar arguments, options, +flags, and functionality to the command line interface. Use the --help flag to +print the help text. + +Type CTRL+D (^D) to exit and generate output or CTRL+C (^C) to abort."# + ) +} + +fn print_warranty() { + println!( + r#"THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION."# + ) +} + +fn print_license() { + println!( + r#"Copyright (C) 2021 Christopher R. Field + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see ."# + ) +} diff --git a/src/asciicast.rs b/src/asciicast.rs index 321ef8a..f27d43f 100644 --- a/src/asciicast.rs +++ b/src/asciicast.rs @@ -30,7 +30,6 @@ pub const DEFAULT_SHELL: &str = "/bin/sh"; pub const DEFAULT_TERM: &str = "xterm-256color"; pub const DEFAULT_WIDTH: &str = "80"; pub const MILLISECONDS_IN_A_SECOND: f64 = 1000.0; -pub const MILLISECONDS_UNITS: &str = "ms"; pub const SHELL_VAR_NAME: &str = "SHELL"; pub const TERM_VAR_NAME: &str = "TERM"; @@ -84,16 +83,6 @@ pub struct Env { pub execute_string_flag: ExecuteStringFlags, } -impl Env { - pub fn shell(&self) -> &str { - &self.shell - } - - pub fn term(&self) -> &str { - &self.term - } -} - impl Default for Env { fn default() -> Self { Self { @@ -249,11 +238,6 @@ impl Asciicast { self } - pub fn append(&mut self, events: &mut Vec) -> &mut Self { - self.events.append(events); - self - } - pub fn append_from(&mut self, sequence: &Sequence) -> &mut Self { let start_delay = sequence .iter() @@ -317,10 +301,6 @@ impl Asciicast { start_delay + input_time + outputs_time } - pub fn events(&self) -> &Vec { - &self.events - } - pub fn write_to(&mut self, mut writer: W) -> Result<()> { self.header.write_to(&mut writer)?; for event in self.events.iter_mut() { diff --git a/src/lib.rs b/src/lib.rs index 4350432..495cffa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -pub mod asciicast; -pub mod sequence; -pub mod termsheets; +pub use crate::app::Cli; + +mod app; +mod asciicast; +mod sequence; +mod termsheets; diff --git a/src/main.rs b/src/main.rs index f16e6ef..6984715 100644 --- a/src/main.rs +++ b/src/main.rs @@ -161,466 +161,8 @@ //! [asciinema player]: https://github.com/asciinema/asciinema-player use anyhow::Result; -use atty::Stream; -use germ::asciicast::Asciicast; -use germ::sequence::{Command, Sequence, Timings, DEFAULT_PROMPT}; -use std::fs::File; -use std::io; -use std::io::{BufRead, BufReader, Read, Write}; -use std::path::PathBuf; -use std::process; -use structopt::clap::{self, value_t, ArgMatches}; +use germ::Cli; use structopt::StructOpt; -use strum::{Display, EnumString, EnumVariantNames, VariantNames}; - -pub const DEFAULT_INTERACTIVE_PROMPT: &str = ">>> "; - -#[derive(Display, Debug, EnumString, EnumVariantNames)] -#[strum(serialize_all = "lowercase")] -enum InputFormats { - Germ, - TermSheets, -} - -impl Default for InputFormats { - fn default() -> Self { - Self::Germ - } -} - -#[derive(Display, Debug, EnumString, EnumVariantNames)] -#[strum(serialize_all = "lowercase")] -enum OutputFormats { - Germ, - TermSheets, - Asciicast, -} - -impl Default for OutputFormats { - fn default() -> Self { - Self::Asciicast - } -} - -#[derive(Debug, StructOpt)] -#[structopt(settings(&[ - clap::AppSettings::NoBinaryName, - clap::AppSettings::DisableHelpFlags, - clap::AppSettings::DisableVersion, - clap::AppSettings::NextLineHelp]), - usage("[FLAGS] [OPTIONS] [INPUT] [OUTPUTS...]") -)] -struct Interactive { - #[structopt(flatten)] - cli: Cli, - - /// Prints the current sequence. - /// - /// The format is determined by the last output format used. - #[structopt(long)] - print: bool, - - /// Prints help information. - #[structopt(short = "h")] - short_help: bool, - - /// Prints more help information. - #[structopt(long = "help")] - long_help: bool, - - /// Prints version information. - #[structopt(short = "V", long = "version")] - version: bool, -} - -#[derive(Debug, StructOpt)] -#[structopt(about = "Generate terminal session recording files without rehearsing and recording")] -struct Cli { - #[structopt(flatten)] - timings: Timings, - - #[structopt(flatten)] - asciicast: Asciicast, - - /// A comment about the command. - /// - /// A line will be "printed" in the terminal session above the prompt and input. - #[structopt(short, long)] - comment: Option, - - /// The prompt to display before the command. - #[structopt(short = "p", long, default_value = DEFAULT_PROMPT, env = "GERM_PROMPT")] - prompt: String, - - /// The prompt displayed in interactive mode. - #[structopt(short ="P", long, default_value = DEFAULT_INTERACTIVE_PROMPT, env = "GERM_INTERACTIVE_PROMPT")] - interactive_prompt: String, - - /// Use the Germ JSON format for the output. - /// - /// This is equivalent to '-O,--output-format germ'. - #[structopt(short = "G")] - use_germ_format: bool, - - /// Prints the license information. - /// - /// This is as recommended by the GPL-3.0 license. - #[structopt(long)] - license: bool, - - /// Prints the warranty information. - /// - /// This is as recommended by the GPL-3.0 license. - #[structopt(long)] - warranty: bool, - - /// The format of the input. - #[structopt( - short = "I", - long, - possible_values = InputFormats::VARIANTS, - case_insensitive = true, - default_value, - value_name = "format", - env = "GERM_INPUT_FORMAT" - )] - input_format: InputFormats, - - /// Input file in the commands JSON format. - /// - /// If not present, then stdin if it is piped or redirected. - #[structopt(short = "i", long = "input", value_name("file"), parse(from_os_str))] - input_file: Option, - - /// The format for the output. - #[structopt( - short = "O", - long, - possible_values = OutputFormats::VARIANTS, - case_insensitive = true, - default_value, - default_value_if("use-germ-format", None, "germ"), - value_name = "format", - env = "GERM_OUTPUT_FORMAT" - )] - output_format: OutputFormats, - - /// Output file, stdout if not present. - /// - /// This is useful if using the application in interactive mode. - #[structopt(short = "o", long = "output", value_name("file"), parse(from_os_str))] - output_file: Option, - - /// The command entered at the prompt. - /// - /// If not present and the -i,--input option is not used, then the - /// application enters an interactive mode where commands are manually - /// entered one at a time within the terminal. If present, then the command - /// is appended to the sequence of commands from any input file or stdin. - /// - /// Note, if present without any output, then the input will be executed - /// within a child shell process and the execution output will be used. - input: Option, - - /// Output from the command. - /// - /// If no output is provided, then the input will be execute within a child - /// shell process and execution output will be used. - outputs: Vec, -} - -impl Cli { - pub fn execute(mut self) -> Result<()> { - if self.license { - print_license(); - return Ok(()); - } - if self.warranty { - print_warranty(); - return Ok(()); - } - let mut sequence = self.read()?; - self.append(&mut sequence)?; - self.write(sequence) - } - - fn read(&self) -> Result { - if let Some(input_file) = &self.input_file { - self.read_from(BufReader::new(File::open(input_file)?)) - } else if atty::is(Stream::Stdin) { - Ok(Sequence::from(self.timings)) - } else { - let stdin = io::stdin(); - self.read_from(stdin) - } - } - - fn read_from(&self, r: R) -> Result { - match self.input_format { - InputFormats::Germ => serde_json::from_reader(r).map_err(anyhow::Error::from), - InputFormats::TermSheets => { - let termsheets: Vec = serde_json::from_reader(r)?; - let mut sequence = Sequence::from(self.timings); - sequence.append( - &mut termsheets - .into_iter() - .map(|c| { - let mut cmd = Command::from(c); - cmd.set_prompt(&self.prompt); - cmd - }) - .collect(), - ); - Ok(sequence) - } - } - } - - fn append(&mut self, sequence: &mut Sequence) -> Result<()> { - if let Some(input) = self.input.as_ref() { - self.append_arguments(sequence, input) - } else if self.input_file.is_none() && atty::is(Stream::Stdin) { - self.append_interactively(sequence) - } else { - Ok(()) - } - } - - fn append_arguments(&self, sequence: &mut Sequence, input: &str) -> Result<()> { - let mut outputs = if self.outputs.is_empty() { - let output = self.execute_cmd(input)?; - vec![std::str::from_utf8(&output.stdout)?.to_owned()] - } else { - self.outputs.clone() - }; - sequence.add({ - let mut cmd = Command::from(input); - cmd.set_comment(self.comment.as_deref()); - cmd.set_prompt(&self.prompt); - cmd.append(&mut outputs); - cmd - }); - Ok(()) - } - - fn append_interactively(&mut self, sequence: &mut Sequence) -> Result<()> { - print_interactive_notice(); - println!(); - let mut stdout = io::stdout(); - stdout.write_all(self.interactive_prompt.as_bytes())?; - stdout.flush()?; - for line in io::stdin().lock().lines() { - let words = shellwords::split(&line.expect("stdin line"))?; - let mut app = Interactive::clap(); - match app.get_matches_from_safe_borrow(words) { - Ok(matches) => { - if matches.is_present("short-help") { - app.write_help(&mut stdout)?; - stdout.write_all(b"\n")?; - } else if matches.is_present("long-help") { - app.write_long_help(&mut stdout)?; - stdout.write_all(b"\n")?; - } else if matches.is_present("short-version") { - app.write_version(&mut stdout)?; - stdout.write_all(b"\n")?; - } else if matches.is_present("long-version") { - app.write_long_version(&mut stdout)?; - stdout.write_all(b"\n")?; - } else if matches.is_present("license") { - print_license(); - } else if matches.is_present("warranty") { - print_warranty(); - } else if matches.is_present("print") { - self.write_to(&mut stdout, &sequence)?; - if !matches!(self.output_format, OutputFormats::Asciicast) { - stdout.write_all(b"\n")?; - } - } else { - self.update_from(&matches); - if let Some(input_file) = matches.value_of("input-file").map(PathBuf::from) - { - sequence.append_from( - self.read_from(BufReader::new(File::open(input_file)?))?, - ); - } - if let Some(input) = matches.value_of("input") { - let mut outputs = if matches.is_present("outputs") { - matches - .values_of("outputs") - .unwrap() - .map(String::from) - .collect() - } else { - let output = self.execute_cmd(&input)?; - stdout.write_all(&output.stdout)?; - vec![std::str::from_utf8(&output.stdout)?.to_owned()] - }; - sequence.add({ - let mut cmd = Command::from(input); - cmd.set_comment( - matches.value_of("comment").map(String::from).as_deref(), - ); - cmd.set_prompt(&self.prompt); - cmd.append(&mut outputs); - cmd - }); - } - } - } - Err(err) => eprintln!("{}", err), - } - stdout.write_all(self.interactive_prompt.as_bytes())?; - stdout.flush()?; - } - stdout.write_all(b"\n")?; - stdout.flush()?; - Ok(()) - } - - fn write(&mut self, sequence: Sequence) -> Result<()> { - let writer: Box = if let Some(output_file) = &self.output_file { - Box::new(File::create(output_file)?) - } else { - Box::new(io::stdout()) - }; - self.write_to(writer, &sequence) - } - - fn write_to(&mut self, mut writer: W, sequence: &Sequence) -> Result<()> { - match self.output_format { - OutputFormats::Germ => { - serde_json::to_writer(&mut writer, &sequence)?; - } - OutputFormats::TermSheets => { - let termsheets: Vec = sequence.into(); - serde_json::to_writer(&mut writer, &termsheets)?; - } - OutputFormats::Asciicast => { - self.asciicast - .append_from(&sequence) - .write_to(&mut writer)?; - } - } - Ok(()) - } - - fn update_from(&mut self, matches: &ArgMatches) { - if matches.occurrences_of("interactive-prompt") != 0 { - self.interactive_prompt = value_t!(matches, "interactive-prompt", String).unwrap(); - } - if matches.occurrences_of("begin-delay") != 0 { - self.timings.begin = value_t!(matches, "begin-delay", f64).unwrap(); - } - if matches.occurrences_of("delay-type-start") != 0 { - self.timings.type_start = value_t!(matches, "delay-type-start", usize).unwrap(); - } - if matches.occurrences_of("delay-type-char") != 0 { - self.timings.type_char = value_t!(matches, "delay-type-char", usize).unwrap(); - } - if matches.occurrences_of("delay-type-submit") != 0 { - self.timings.type_submit = value_t!(matches, "delay-type-start", usize).unwrap(); - } - if matches.occurrences_of("delay-output-line") != 0 { - self.timings.output_line = value_t!(matches, "delay-output-line", usize).unwrap(); - } - if matches.occurrences_of("end-delay") != 0 { - self.timings.end = value_t!(matches, "end-delay", f64).unwrap(); - } - if matches.occurrences_of("title") != 0 { - self.asciicast.header.title = value_t!(matches, "title", String).ok(); - } - if matches.occurrences_of("width") != 0 { - self.asciicast.header.width = value_t!(matches, "width", usize).unwrap(); - } - if matches.occurrences_of("height") != 0 { - self.asciicast.header.height = value_t!(matches, "height", usize).unwrap(); - } - if matches.occurrences_of("input-format") != 0 { - self.input_format = value_t!(matches, "input-format", InputFormats).unwrap(); - } - if matches.occurrences_of("output-format") != 0 { - self.output_format = value_t!(matches, "output-format", OutputFormats).unwrap(); - } - if matches.occurrences_of("output-file") != 0 { - self.output_file = value_t!(matches, "output-file", PathBuf).ok(); - } - if matches.occurrences_of("prompt") != 0 { - self.prompt = value_t!(matches, "prompt", String).unwrap(); - } - if matches.occurrences_of("speed") != 0 { - self.timings.speed = value_t!(matches, "speed", f64).unwrap(); - } - if matches.occurrences_of("shell") != 0 { - self.asciicast.header.env.shell = value_t!(matches, "shell", String).unwrap(); - } - if matches.occurrences_of("term") != 0 { - self.asciicast.header.env.term = value_t!(matches, "shell", String).unwrap(); - } - if matches.occurrences_of("stdin") != 0 { - self.asciicast.stdin = true; - } - if matches.occurrences_of("use-germ-format") != 0 { - self.use_germ_format = true; - } - } - - fn execute_cmd(&self, input: &str) -> Result { - process::Command::new(self.asciicast.header.env.shell()) - .args(&[ - &format!("{}", self.asciicast.header.env.execute_string_flag), - input, - ]) - .output() - .map_err(anyhow::Error::from) - } -} - -fn print_interactive_notice() { - println!( - r#"Copyright (C) 2021 Christopher R. Field -This program comes with ABSOLUTELY NO WARRANTY; for details use the `--warranty` -flag. This is free software, and you are welcome to redistirbute it under -certain conditions; use the `--license` flag for details. - -You have entered interactive mode. The prompt has similar arguments, options, -flags, and functionality to the command line interface. Use the --help flag to -print the help text. - -Type CTRL+D (^D) to exit and generate output or CTRL+C (^C) to abort."# - ) -} - -fn print_warranty() { - println!( - r#"THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION."# - ) -} - -fn print_license() { - println!( - r#"Copyright (C) 2021 Christopher R. Field - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see ."# - ) -} fn main() -> Result<()> { Cli::from_args().execute()