nreplops_tool/
outputs.rs

1// outputs.rs
2// Copyright 2022 Matti Hänninen
3//
4// Licensed under the Apache License, Version 2.0 (the "License"); you may not
5// use this file except in compliance with the License. You may obtain a copy of
6// the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13// License for the specific language governing permissions and limitations under
14// the License.
15
16use 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  // Receives our "internal" normal output
137  pub stdout: Output,
138  // Receives our "internal" error output
139  pub stderr: Output,
140  // Receives nREPL's standard output
141  pub nrepl_stdout: Option<Output>,
142  // Receives nREPL's standard error
143  pub nrepl_stderr: Option<Output>,
144  // Receives result forms
145  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// Sources of output on the nREPL's side
255#[derive(Debug)]
256enum Src {
257  StdOut,
258  StdErr,
259  Results,
260}
261
262// Local destinations for output
263#[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        // XXX(soija) I have not thought out the implications of this aggressive
282        //            error handling.
283        &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}