From f6076673276091d1e8bdf25672b578512c98e310 Mon Sep 17 00:00:00 2001 From: Christopher Field Date: Wed, 24 Mar 2021 12:55:03 -0400 Subject: [PATCH] Refactor to more modules The "app" components are moved to their own `app` module. This makes the `main.rs` very short and sweet, which leaves plenty of room for writing application-level documentation in the form of module-level doc comments to be added without making the file too large. This also allows the visibility of some modules to be changed to private, which highlighted some functions, methods, and constants were unused. This is related to #6 and #7. --- Cargo.lock | 1 + Cargo.toml | 1 + src/app.rs | 478 +++++++++++++++++++++++++++++++++++++++++++++++ src/asciicast.rs | 20 -- src/lib.rs | 9 +- src/main.rs | 460 +-------------------------------------------- 6 files changed, 487 insertions(+), 482 deletions(-) create mode 100644 src/app.rs 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()