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()