use std::{
cell::{RefCell, RefMut},
collections::HashMap,
fmt, fs,
io::{self, IsTerminal, Write},
os::fd::AsRawFd,
path::{self, Path},
rc::Rc,
};
use crate::{
cli::{self, IoArg},
clojure::lex,
error::Error,
pprint::ClojureResultPrinter,
};
#[derive(Clone, Debug)]
pub enum Output {
StdOut(StdType),
StdErr(StdType),
File {
file: Rc<RefCell<io::BufWriter<fs::File>>>,
path: Box<Path>,
},
}
#[derive(Clone, Debug)]
pub enum StdType {
Terminal(u16),
TerminalWithoutWidth,
Pipe,
}
impl Output {
pub fn writer(&self) -> OutputWriter<'_> {
match *self {
Output::StdOut(..) => OutputWriter::StdOut,
Output::StdErr(..) => OutputWriter::StdErr,
Output::File { ref file, ref path } => OutputWriter::File {
file: file.borrow_mut(),
path,
},
}
}
pub fn is_terminal(&self) -> bool {
matches!(
self,
Output::StdOut(StdType::Terminal(..))
| Output::StdOut(StdType::TerminalWithoutWidth)
| Output::StdErr(StdType::Terminal(..))
| Output::StdErr(StdType::TerminalWithoutWidth)
)
}
pub fn width(&self) -> Option<u16> {
match *self {
Output::StdOut(StdType::Terminal(width))
| Output::StdErr(StdType::Terminal(width)) => Some(width),
_ => None,
}
}
pub fn generate_error(&self, _: io::Error) -> Error {
match self {
Output::StdOut { .. } => Error::CannotWriteStdOut,
Output::StdErr { .. } => Error::CannotWriteStdErr,
Output::File { path, .. } => {
Error::CannotWriteFile(path.to_string_lossy().to_string())
}
}
}
}
#[derive(Debug)]
pub enum OutputWriter<'a> {
StdOut,
StdErr,
File {
file: RefMut<'a, io::BufWriter<fs::File>>,
path: &'a Path,
},
}
impl<'a> Write for OutputWriter<'a> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match *self {
OutputWriter::StdOut => io::stdout().lock().write(buf),
OutputWriter::StdErr => io::stderr().lock().write(buf),
OutputWriter::File { ref mut file, .. } => file.write(buf),
}
}
fn write_fmt(&mut self, fmt: fmt::Arguments) -> io::Result<()> {
match *self {
OutputWriter::StdOut => io::stdout().lock().write_fmt(fmt),
OutputWriter::StdErr => io::stderr().lock().write_fmt(fmt),
OutputWriter::File { ref mut file, .. } => file.write_fmt(fmt),
}
}
fn flush(&mut self) -> io::Result<()> {
match *self {
OutputWriter::StdOut => io::stdout().lock().flush(),
OutputWriter::StdErr => io::stderr().lock().flush(),
OutputWriter::File { ref mut file, .. } => file.flush(),
}
}
}
#[derive(Debug)]
pub enum OutputTarget<'a> {
StdOut,
StdErr,
File(&'a Path),
}
#[derive(Debug)]
pub struct Outputs {
pub stdout: Output,
pub stderr: Output,
pub nrepl_stdout: Option<Output>,
pub nrepl_stderr: Option<Output>,
pub nrepl_results: Option<NreplResultsSink>,
}
impl Outputs {
pub fn try_from_args(args: &cli::Args) -> Result<Self, Error> {
fn err_cannot_write_file(path: &path::Path) -> Error {
Error::CannotWriteFile(path.to_string_lossy().to_string())
}
fn file_dst_from_path(path: &path::Path) -> Result<Dst, Error> {
path
.canonicalize()
.map(|p| Dst::File(p.into()))
.map_err(|_| err_cannot_write_file(path))
}
let mut logical_connections = HashMap::<Dst, Vec<Src>>::new();
let mut connect = |source: Src, sink: Dst| {
logical_connections
.entry(sink)
.or_insert_with(Vec::new)
.push(source);
};
match args.stdout_to {
Some(IoArg::Pipe) => connect(Src::StdOut, Dst::StdOut),
Some(IoArg::File(ref p)) => connect(Src::StdOut, file_dst_from_path(p)?),
None => (),
}
match args.stderr_to {
Some(IoArg::Pipe) => connect(Src::StdErr, Dst::StdErr),
Some(IoArg::File(ref p)) => connect(Src::StdErr, file_dst_from_path(p)?),
None => (),
}
match args.results_to {
Some(IoArg::Pipe) => connect(Src::Results, Dst::StdOut),
Some(IoArg::File(ref p)) => connect(Src::Results, file_dst_from_path(p)?),
None => (),
}
fn determine_std_type<S>(s: S) -> StdType
where
S: IsTerminal + AsRawFd,
{
use terminal_size::{terminal_size_using_fd, Width};
if s.is_terminal() {
if let Some((Width(w), _)) = terminal_size_using_fd(s.as_raw_fd()) {
StdType::Terminal(w)
} else {
StdType::TerminalWithoutWidth
}
} else {
StdType::Pipe
}
}
let stderr = Output::StdErr(determine_std_type(io::stderr()));
let stdout = Output::StdOut(determine_std_type(io::stdout()));
let mut nrepl_stdout = None;
let mut nrepl_stderr = None;
let mut nrepl_results = None;
for (sink, sources) in logical_connections.into_iter() {
let output = match sink {
Dst::StdOut => stdout.clone(),
Dst::StdErr => stderr.clone(),
Dst::File(path) => {
let f = fs::File::create(&path)
.map_err(|_| err_cannot_write_file(&path))?;
let w = io::BufWriter::new(f);
Output::File {
file: Rc::new(RefCell::new(w)),
path,
}
}
};
for source in sources.into_iter() {
match source {
Src::StdOut => nrepl_stdout = Some(output.clone()),
Src::StdErr => nrepl_stderr = Some(output.clone()),
Src::Results => {
let pretty = args.pretty.to_bool(output.is_terminal());
let color = args.color.to_bool(output.is_terminal());
let width = output.width().unwrap_or(80);
nrepl_results = Some(NreplResultsSink {
output: output.clone(),
formatter: if pretty || color {
Some(ClojureResultPrinter::new(pretty, color, width))
} else {
None
},
})
}
}
}
}
Ok(Self {
stdout,
stderr,
nrepl_stdout,
nrepl_stderr,
nrepl_results,
})
}
}
#[derive(Debug)]
enum Src {
StdOut,
StdErr,
Results,
}
#[derive(Debug, PartialEq, Eq, Hash)]
enum Dst {
StdOut,
StdErr,
File(Box<Path>),
}
#[derive(Debug)]
pub struct NreplResultsSink {
output: Output,
formatter: Option<ClojureResultPrinter>,
}
impl NreplResultsSink {
pub fn output(&self, clojure: &str) -> Result<(), Error> {
if let Some(ref f) = self.formatter {
f.print(
&mut self.output.writer(),
&lex::lex(clojure).map_err(|e| Error::FailedToParseResult(e.into()))?,
)
} else {
writeln!(self.output.writer(), "{}", clojure)
}
.map_err(|e| self.output.generate_error(e))
}
}