1use std::{
17 cell::{RefCell, RefMut},
18 collections::HashMap,
19 fmt, fs,
20 io::{self, IsTerminal, Write},
21 os::fd::AsRawFd,
22 path::{self, Path},
23 rc::Rc,
24};
25
26use crate::{
27 cli::{self, IoArg},
28 clojure::lex,
29 error::Error,
30 pprint::ClojureResultPrinter,
31};
32
33#[derive(Clone, Debug)]
34pub enum Output {
35 StdOut(StdType),
36 StdErr(StdType),
37 File {
38 file: Rc<RefCell<io::BufWriter<fs::File>>>,
39 path: Box<Path>,
40 },
41}
42
43#[derive(Clone, Debug)]
44pub enum StdType {
45 Terminal(u16),
46 TerminalWithoutWidth,
47 Pipe,
48}
49
50impl Output {
51 pub fn writer(&self) -> OutputWriter<'_> {
52 match *self {
53 Output::StdOut(..) => OutputWriter::StdOut,
54 Output::StdErr(..) => OutputWriter::StdErr,
55 Output::File { ref file, ref path } => OutputWriter::File {
56 file: file.borrow_mut(),
57 path,
58 },
59 }
60 }
61
62 pub fn is_terminal(&self) -> bool {
63 matches!(
64 self,
65 Output::StdOut(StdType::Terminal(..))
66 | Output::StdOut(StdType::TerminalWithoutWidth)
67 | Output::StdErr(StdType::Terminal(..))
68 | Output::StdErr(StdType::TerminalWithoutWidth)
69 )
70 }
71
72 pub fn width(&self) -> Option<u16> {
73 match *self {
74 Output::StdOut(StdType::Terminal(width))
75 | Output::StdErr(StdType::Terminal(width)) => Some(width),
76 _ => None,
77 }
78 }
79
80 pub fn generate_error(&self, _: io::Error) -> Error {
81 match self {
82 Output::StdOut { .. } => Error::CannotWriteStdOut,
83 Output::StdErr { .. } => Error::CannotWriteStdErr,
84 Output::File { path, .. } => {
85 Error::CannotWriteFile(path.to_string_lossy().to_string())
86 }
87 }
88 }
89}
90
91#[derive(Debug)]
92pub enum OutputWriter<'a> {
93 StdOut,
94 StdErr,
95 File {
96 file: RefMut<'a, io::BufWriter<fs::File>>,
97 path: &'a Path,
98 },
99}
100
101impl<'a> Write for OutputWriter<'a> {
102 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
103 match *self {
104 OutputWriter::StdOut => io::stdout().lock().write(buf),
105 OutputWriter::StdErr => io::stderr().lock().write(buf),
106 OutputWriter::File { ref mut file, .. } => file.write(buf),
107 }
108 }
109
110 fn write_fmt(&mut self, fmt: fmt::Arguments) -> io::Result<()> {
111 match *self {
112 OutputWriter::StdOut => io::stdout().lock().write_fmt(fmt),
113 OutputWriter::StdErr => io::stderr().lock().write_fmt(fmt),
114 OutputWriter::File { ref mut file, .. } => file.write_fmt(fmt),
115 }
116 }
117
118 fn flush(&mut self) -> io::Result<()> {
119 match *self {
120 OutputWriter::StdOut => io::stdout().lock().flush(),
121 OutputWriter::StdErr => io::stderr().lock().flush(),
122 OutputWriter::File { ref mut file, .. } => file.flush(),
123 }
124 }
125}
126
127#[derive(Debug)]
128pub enum OutputTarget<'a> {
129 StdOut,
130 StdErr,
131 File(&'a Path),
132}
133
134#[derive(Debug)]
135pub struct Outputs {
136 pub stdout: Output,
138 pub stderr: Output,
140 pub nrepl_stdout: Option<Output>,
142 pub nrepl_stderr: Option<Output>,
144 pub nrepl_results: Option<NreplResultsSink>,
146}
147
148impl Outputs {
149 pub fn try_from_args(args: &cli::Args) -> Result<Self, Error> {
150 fn err_cannot_write_file(path: &path::Path) -> Error {
151 Error::CannotWriteFile(path.to_string_lossy().to_string())
152 }
153
154 fn file_dst_from_path(path: &path::Path) -> Result<Dst, Error> {
155 path
156 .canonicalize()
157 .map(|p| Dst::File(p.into()))
158 .map_err(|_| err_cannot_write_file(path))
159 }
160
161 let mut logical_connections = HashMap::<Dst, Vec<Src>>::new();
162
163 let mut connect = |source: Src, sink: Dst| {
164 logical_connections
165 .entry(sink)
166 .or_insert_with(Vec::new)
167 .push(source);
168 };
169
170 match args.stdout_to {
171 Some(IoArg::Pipe) => connect(Src::StdOut, Dst::StdOut),
172 Some(IoArg::File(ref p)) => connect(Src::StdOut, file_dst_from_path(p)?),
173 None => (),
174 }
175 match args.stderr_to {
176 Some(IoArg::Pipe) => connect(Src::StdErr, Dst::StdErr),
177 Some(IoArg::File(ref p)) => connect(Src::StdErr, file_dst_from_path(p)?),
178 None => (),
179 }
180 match args.results_to {
181 Some(IoArg::Pipe) => connect(Src::Results, Dst::StdOut),
182 Some(IoArg::File(ref p)) => connect(Src::Results, file_dst_from_path(p)?),
183 None => (),
184 }
185
186 fn determine_std_type<S>(s: S) -> StdType
187 where
188 S: IsTerminal + AsRawFd,
189 {
190 use terminal_size::{terminal_size_using_fd, Width};
191 if s.is_terminal() {
192 if let Some((Width(w), _)) = terminal_size_using_fd(s.as_raw_fd()) {
193 StdType::Terminal(w)
194 } else {
195 StdType::TerminalWithoutWidth
196 }
197 } else {
198 StdType::Pipe
199 }
200 }
201
202 let stderr = Output::StdErr(determine_std_type(io::stderr()));
203 let stdout = Output::StdOut(determine_std_type(io::stdout()));
204
205 let mut nrepl_stdout = None;
206 let mut nrepl_stderr = None;
207 let mut nrepl_results = None;
208
209 for (sink, sources) in logical_connections.into_iter() {
210 let output = match sink {
211 Dst::StdOut => stdout.clone(),
212 Dst::StdErr => stderr.clone(),
213 Dst::File(path) => {
214 let f = fs::File::create(&path)
215 .map_err(|_| err_cannot_write_file(&path))?;
216 let w = io::BufWriter::new(f);
217 Output::File {
218 file: Rc::new(RefCell::new(w)),
219 path,
220 }
221 }
222 };
223 for source in sources.into_iter() {
224 match source {
225 Src::StdOut => nrepl_stdout = Some(output.clone()),
226 Src::StdErr => nrepl_stderr = Some(output.clone()),
227 Src::Results => {
228 let pretty = args.pretty.to_bool(output.is_terminal());
229 let color = args.color.to_bool(output.is_terminal());
230 let width = output.width().unwrap_or(80);
231 nrepl_results = Some(NreplResultsSink {
232 output: output.clone(),
233 formatter: if pretty || color {
234 Some(ClojureResultPrinter::new(pretty, color, width))
235 } else {
236 None
237 },
238 })
239 }
240 }
241 }
242 }
243
244 Ok(Self {
245 stdout,
246 stderr,
247 nrepl_stdout,
248 nrepl_stderr,
249 nrepl_results,
250 })
251 }
252}
253
254#[derive(Debug)]
256enum Src {
257 StdOut,
258 StdErr,
259 Results,
260}
261
262#[derive(Debug, PartialEq, Eq, Hash)]
264enum Dst {
265 StdOut,
266 StdErr,
267 File(Box<Path>),
268}
269
270#[derive(Debug)]
271pub struct NreplResultsSink {
272 output: Output,
273 formatter: Option<ClojureResultPrinter>,
274}
275
276impl NreplResultsSink {
277 pub fn output(&self, clojure: &str) -> Result<(), Error> {
278 if let Some(ref f) = self.formatter {
279 f.print(
280 &mut self.output.writer(),
281 &lex::lex(clojure).map_err(|e| Error::FailedToParseResult(e.into()))?,
284 )
285 } else {
286 writeln!(self.output.writer(), "{}", clojure)
287 }
288 .map_err(|e| self.output.generate_error(e))
289 }
290}