conciliator/core.rs
1/*! Core functionality: dealing with buffers of colored terminal output
2
3Text is never written directly to the terminal, it is written into a buffer first, which then get printed.
4The buffer is just a simple `Vec<u8>` (and a boolean to store whether colors are enabled), implementing [`Write`] and [`Paint`] to allow printing colored text.
5
6Dealing with the buffers is somewhat unidiomatic, so it bears some explanation.
7Here are four different ways to write the same status message to the terminal:
8```rust,no_run
9use std::io::Write;
10use conciliator::Conciliator;
11
12let con = conciliator::init();
13let hello = "Hello";
14
15con.status("Hello!");
16con.status(format_args!("{hello}!"));
17write!(con.status(..), "{hello}!").unwrap();
18con.status(hello).push("!");
19```
20The [`con.status( … )`](crate::Conciliator::status) method returns a [`Line`], which:
21- contains the status tag (e.g. `[ > ] `) and whatever was passed into it (`..` is used to specify "nothing", see [`InitialContent`]),
22- implements [`Paint`] (and thus [`Write`]),
23- automatically appends a newline (`\n`) character when it gets printed (unless overridden with [`Line::no_newline`]),
24
25Finally, to avoid having to explicitly print every [`Line`], it holds a reference to the [`Stream`] it was obtained from, and the **destructor uses this reference to print the buffer** to the terminal.
26
27*/
28use std::env;
29use std::fmt::Arguments;
30use std::io::{
31 self,
32 IsTerminal,
33 Write,
34 Result as IoResult
35};
36use std::ops::{
37 Deref,
38 DerefMut
39};
40
41use crate::term::EmitEscapes;
42use crate::style::Paint;
43use crate::push::{
44 Inline,
45 Pushable
46};
47use super::Conciliator;
48
49/// Wrapper around a standard output stream (`stdout` or `stderr`)
50///
51/// Does not implement [`Write`] or [`Paint`] itself.
52///
53/// While a buffer is written to the underlying stream, it is *locked* (i.e. [`io::Stdout::lock`]).
54/// This is to ensure there is no interleaving of the contents of buffers, even in cases where there are multiple [`Stream`]s referencing the same standard output stream.
55/// This idea is taken from [termcolor](https://crates.io/crates/termcolor).
56///
57/// #### Making `cargo test` capture output
58///
59/// Unfortunately, `cargo test`, will only capture output produced by the [`print!`], [`println!`], [`eprint!`], and [`eprintln!`] macros (see [rust#12309] and [rust#90785]).
60/// To accommodate this, *printing has to work and behave differently during tests*: going through the [`print!`] or [`eprint!`] macro instead of directly writing the buffer content to the output stream.
61///
62/// Because this crate will not be compiled with `cfg(test)` when it is being compiled as a dependency of another crate, it is not possible to automatically choose the correct behavior.
63/// Instead, it is controlled by the `test_capture` feature flag, which will unilaterally select the *worse but capture-able* behavior if it is enabled.
64/// As mentioned in the [crate-level documentation](crate#capture-output-during-cargo-test), this is why you should **only** enable `test_capture` on a `[dev-dependencies]` entry and **never** for a regular dependency.
65///
66/// Using the macros to write output is worse because it somewhat arbitrarily restricts the content to be valid UTF-8 like a `&str` or `String`.
67/// [`String::from_utf8_lossy`] is used (again, only if the `test_capture` feature is enabled) to enforce this on the content of [`Buffer`], which implements [`Write`] and as such can contain arbitrary byte sequences.
68///
69/// This means that output during tests, whether captured or not, will have "invalid UTF-8 sequences" replaced "with [`U+FFFD REPLACEMENT CHARACTER`][U+FFFD], which looks like this: �".
70/// See [`String::from_utf8_lossy`] for further details.
71///
72/// [rust#12309]: https://github.com/rust-lang/rust/issues/12309
73/// [rust#90785]: https://github.com/rust-lang/rust/issues/90785
74/// [U+FFFD]: char::REPLACEMENT_CHARACTER
75pub struct Stream {
76 inner: StdStream,
77 should_color: bool,
78}
79
80enum StdStream {
81 Out(io::Stdout),
82 Err(io::Stderr)
83}
84
85/// Buffer for colored text
86///
87/// A buffer stores text and color escape codes for printing to an output stream.
88/// Actually writing the escape codes can be disabled at runtime (i.e. when the underlying stream is not attached to a TTY).
89/// See [`Paint`] for convenient methods to append colored text.
90///
91/// This has also been taken and adapted from [termcolor](https://crates.io/crates/termcolor).
92pub struct Buffer {
93 inner: Vec<u8>,
94 should_color: bool
95}
96
97/// Wrap a [`Buffer`] to print it and a newline when dropped
98///
99/// This struct "borrows" (figuratively) a [`Buffer`] from a [`Stream`].
100/// It holds a reference to this [`Stream`], and the **destructor uses this reference to print the buffer** to the terminal.
101///
102/// Before the buffer is printed, a **newline character is appended** to it.
103/// This can be avoided by using [`Line::no_newline`] to turn it into a [`NoNewLine`].
104pub struct Line<'s> {
105 /// `Option` because we might need to discard it
106 buffer: Option<Buffer>,
107 stream: &'s Stream
108}
109
110/// Wrap a [`Buffer`] to print it *without* a newline when dropped
111///
112/// This struct is obtained from [`Line::no_newline`] and apart from *not* appending the newline automatically, it is equivalent to [`Line`].
113/// This means it also **prints itself when dropped**.
114pub struct NoNewLine<'s> {
115 /// `Option` because we might need to discard it
116 buffer: Option<Buffer>,
117 stream: &'s Stream
118}
119
120/// Initial content for a buffer
121///
122/// This wouldn't actually be the *first* thing in the buffer if it has been
123/// obtained using a method that adds the tag, which comes first.
124///
125/// Unfortunately it is not possible to implement this trait for all
126/// [`std::fmt::Display`] types.
127/// It is implemented for [`&str`] however, and that is usually enough.
128/// If it isn't, the [`Wrap::Plain`](crate::push::Wrap::Plain) wrapper can be used, because this trait is implemented for all [`Inline`] types.
129///
130/// To provide no initial content, use one of the following:
131/// ```rust,no_run
132/// # let con = conciliator::init();
133/// # use conciliator::Conciliator;
134/// con.status(..);
135/// con.status(());
136/// con.status(None);
137/// ```
138pub trait InitialContent {
139 /// Initialize the buffer
140 fn init_buffer(&self, buffer: &mut Buffer);
141}
142
143/// Helper trait for implementing the [`Conciliator`]
144///
145/// This trait establishes the borrow relation and crucially requires the `Line` type to implement [`Paint`]
146pub trait GetLine<'l> {
147 /// Type of the line buffer (does not have to be the struct [`Line`])
148 type Line: 'l + Deref<Target = Buffer> + DerefMut;
149 /// Obtain a new line buffer
150 fn get_line(&'l self) -> Self::Line;
151}
152
153/*
154 * STD STREAM
155 */
156
157impl StdStream {
158 pub(crate) fn clone(&self) -> Self {
159 match self {
160 Self::Out(_) => Self::Out(io::stdout()),
161 Self::Err(_) => Self::Err(io::stderr()),
162 }
163 }
164 fn is_terminal(&self) -> bool {
165 match self {
166 Self::Out(out) => out.is_terminal(),
167 Self::Err(err) => err.is_terminal()
168 }
169 }
170}
171
172/*
173 * STREAM
174 */
175
176impl Stream {
177 pub(crate) fn clone(&self) -> Self {
178 Self {
179 inner: self.inner.clone(),
180 ..*self
181 }
182 }
183 pub(crate) fn colors_enabled(&self) -> bool {self.should_color}
184 /// Initialize on stdout
185 ///
186 /// Colors can be enabled (`Some(true)`) or disabled (`Some(false)`) manually.
187 ///
188 /// Otherwise (`None`), colors will only be enabled if:
189 /// - the stdout stream is a TTY, **and**
190 /// - the `TERM` environment variable is set to something other than `dumb`, **and**
191 /// - the environment variable `NO_COLOR` is *not* set.
192 ///
193 /// This is the algorithm as used by [termcolor](https://crates.io/crates/termcolor)
194 pub fn stdout(color_override: Option<bool>) -> Self {
195 let inner = StdStream::Out(io::stdout());
196 let should_color = color_override.unwrap_or_else(
197 || inner.is_terminal() && Self::should_color()
198 );
199 Self {inner, should_color}
200 }
201 /// Initialize on stderr
202 ///
203 /// Colors can be enabled (`Some(true)`) or disabled (`Some(false)`) manually.
204 ///
205 /// Otherwise (`None`), colors will only be enabled if:
206 /// - the stderr stream is a TTY, **and**
207 /// - the `TERM` environment variable is set to something other than `dumb`, **and**
208 /// - the environment variable `NO_COLOR` is *not* set.
209 ///
210 /// This is the algorithm as used by [termcolor](https://crates.io/crates/termcolor)
211 pub fn stderr(color_override: Option<bool>) -> Self {
212 let inner = StdStream::Err(io::stderr());
213 let should_color = color_override.unwrap_or_else(
214 || inner.is_terminal() && Self::should_color()
215 );
216 Self {inner, should_color}
217 }
218 /// Get a new [`Buffer`] from this stream, with matching color status
219 pub fn buffer(&self) -> Buffer {
220 Buffer::new(self.should_color)
221 }
222
223 /// Get a new [`Line`] associated with this stream, with matching color status
224 pub fn line(&'_ self) -> Line<'_> {
225 Line::new(self)
226 }
227
228 /// Manually print a buffer by reference
229 pub fn print_buffer(&self, buffer: &Buffer) -> io::Result<()> {
230 #[cfg(not(any(test, feature = "test_capture")))] {
231 // The lock() seems to be redundant, write_all would lock anyway
232 match &self.inner {
233 StdStream::Out(out) => out.lock().write_all(&buffer.inner),
234 StdStream::Err(err) => err.lock().write_all(&buffer.inner)
235 }
236 }
237 #[cfg(any(test, feature = "test_capture"))] {
238 let s = String::from_utf8_lossy(&buffer.inner);
239 match &self.inner {
240 StdStream::Out(_out) => print!("{s}"),
241 StdStream::Err(_err) => eprint!("{s}")
242 }
243 Ok(())
244 }
245 }
246
247 /// Manually print a byte slice
248 pub fn print_bytes(&self, buffer: &[u8]) -> io::Result<()> {
249 #[cfg(not(any(test, feature = "test_capture")))] {
250 // The lock() seems to be redundant, write_all would lock anyway
251 match &self.inner {
252 StdStream::Out(out) => out.lock().write_all(buffer),
253 StdStream::Err(err) => err.lock().write_all(buffer)
254 }
255 }
256 #[cfg(any(test, feature = "test_capture"))] {
257 let s = String::from_utf8_lossy(buffer);
258 match &self.inner {
259 StdStream::Out(_out) => print!("{s}"),
260 StdStream::Err(_err) => eprint!("{s}")
261 }
262 Ok(())
263 }
264 }
265
266 /// Flush the underlying stream
267 pub fn flush(&self) -> io::Result<()> {
268 match &self.inner {
269 StdStream::Out(out) => out.lock().flush(),
270 StdStream::Err(err) => err.lock().flush()
271 }
272 }
273
274 /// Manually flush the underlying stream after printing the buffer
275 ///
276 /// This is used by the [`NoNewLine`] buffer wrapper because it cannot rely on a newline character to trigger "automatic" flushing.
277 /// Provided as a separate method to avoid locking the output stream twice when using a shared reference.
278 #[cfg(not(any(test, feature = "test_capture")))]
279 pub(crate) fn print_buffer_and_flush(&self, buffer: &Buffer)
280 -> io::Result<()>
281 {
282 match &self.inner {
283 StdStream::Out(out) => {
284 let mut out = out.lock();
285 out.write_all(&buffer.inner)?;
286 out.flush()
287 },
288 StdStream::Err(err) => {
289 let mut err = err.lock();
290 err.write_all(&buffer.inner)?;
291 err.flush()
292 }
293 }
294 }
295
296 /// Manually flush the underlying stream after printing the buffer
297 ///
298 /// This is used by the [`NoNewLine`] buffer wrapper because it cannot rely on a newline character to trigger "automatic" flushing.
299 /// Provided as a separate method to avoid locking the output stream twice when using a shared reference.
300 #[cfg(any(test, feature = "test_capture"))]
301 pub(crate) fn print_buffer_and_flush(&self, buffer: &Buffer)
302 -> io::Result<()>
303 {
304 let s = String::from_utf8_lossy(&buffer.inner);
305 match &self.inner {
306 StdStream::Out(out) => {
307 let mut out = out.lock();
308 print!("{s}");
309 out.flush()
310 },
311 StdStream::Err(err) => {
312 let mut err = err.lock();
313 eprint!("{s}");
314 err.flush()
315 }
316 }
317 }
318
319 fn should_color() -> bool {
320 //credit: termcolor
321 match env::var_os("TERM") {
322 Some(var) if var == "dumb" => false,
323 None => false,
324 Some(_) => env::var_os("NO_COLOR").is_none()
325 }
326 }
327}
328
329impl<'l> GetLine<'l> for Stream {
330 type Line = Line<'l>;
331 fn get_line(&'l self) -> Self::Line {self.line()}
332}
333
334impl Conciliator for Stream {}
335
336/*
337 * BUFFER
338 */
339
340impl Buffer {
341 /// Create a new buffer for colored text. If `should_color` is false, all attempts at setting colors or styles via the [`Paint`] trait will be silently ignored.
342 pub fn new(should_color: bool) -> Self {
343 Self {
344 inner: Vec::new(),
345 should_color
346 }
347 }
348 /// Append `thing` onto the buffer using the [`Inline`] trait
349 pub fn push_inline<T: Inline>(&mut self, thing: &T) -> &mut Self {
350 thing.inline(self);
351 self
352 }
353 /// Append `thing` onto the buffer using **either** [`Inline`] or [`Display`](std::fmt::Display), doesn't work if both are implemented for `T`
354 ///
355 /// [`Pushable`] is blanket-implemented for **both** [`Inline`] and [`Display`](std::fmt::Display) types.
356 /// This would normally not be possible, because both blanket implementations could apply to the same type, thereby causing a conflict (Rust does not have any specialization or way to express `where T: NOT Trait`).
357 /// So instead, this uses a trick that relies on a "marker type" being inferred.
358 ///
359 ///
360 /// This workaround is described in detail on [`Pushable`], but in short:
361 /// - [`Pushable`] has a type parameter `M` (i.e. `trait Pushable<M>`) that is not used except to make distinct implementations possible for the same type
362 /// - `impl<T: Display> Pushable<marker::AsDisplay> for T`
363 /// - `impl<T: Inline> Pushable<marker::AsInline> for T`
364 /// - `push` is implemented for *any* `T: Pushable<M>` with *any* `M`
365 /// - when there is only one applicable implementation, the marker type `M` can be inferred and won't need to be specified
366 /// - Surprisingly, it just works!
367 ///
368 /// If, however, there are multiple applicable implementations (i.e. for a type implements [`Inline`] *and* [`Display`](std::fmt::Display)), the type cannot be inferred: `type annotations needed`.
369 /// In this case, it is easier to just use the more specific function instead of bothering with the type annotations.
370 /// (For the time being, the marker types are private, so type annotations are also impossible.)
371 ///
372 ///```
373 /// use conciliator::{Conciliator, Wrap};
374 /// let con = conciliator::init();
375 /// con.line(..)
376 /// .push("Test") // &str,
377 /// .push(123) // i32 and
378 /// .push(' ') // char all implement Display
379 /// .push(Wrap::Alpha(":^)")); // Wrap implements Inline
380 ///```
381 pub fn push<M, T: Pushable<M>>(&mut self, thing: T) -> &mut Self {
382 thing.push_into(self);
383 self
384 }
385
386 /// Clear the contents of the [`Buffer`] so that it can be reused
387 ///
388 /// Note that if you use this method through the [`Deref`] on a [`Line`], it will *still print the newline character when dropped*.
389 /// You may want to use [`Line::discard`] instead.
390 pub(crate) fn clear(&mut self) {self.inner.clear()}
391}
392
393
394impl Write for Buffer {
395 fn write(&mut self, buf: &[u8]) -> IoResult<usize> {
396 self.inner.write(buf)
397 }
398 fn flush(&mut self) -> IoResult<()> {
399 self.inner.flush()
400 }
401}
402
403impl EmitEscapes for Buffer {
404 fn escapes_recognized(&self) -> bool {self.should_color}
405}
406
407
408/*
409 * LINE
410 */
411
412impl<'s> Line<'s> {
413 /// "Borrow" a new `Line` buffer from the [`Stream`]
414 pub fn new(stream: &'s Stream) -> Self {
415 let buffer = Some(Buffer::new(stream.should_color));
416 Self {buffer, stream}
417 }
418 /// Print this `Line` into the [`Stream`]. This is equivalent to dropping.
419 pub fn print(self) {}
420 /// Discard and drop this buffer without printing it
421 pub fn discard(mut self) {self.buffer.take();}
422 /// No longer add the newline when dropped
423 pub fn no_newline(mut self) -> NoNewLine<'s> {
424 let buffer = self.buffer.take().unwrap();
425 NoNewLine {stream: self.stream, buffer: Some(buffer)}
426 }
427}
428
429impl<'s> Drop for Line<'s> {
430 /// The `Line` prints itself to the [`Stream`] when dropped
431 fn drop(&mut self) {
432 if let Some(buffer) = self.buffer.as_mut() {
433 writeln!(buffer, ).unwrap();
434 self.stream.print_buffer(buffer).unwrap();
435 }
436 }
437}
438impl<'s> Deref for Line<'s> {
439 type Target = Buffer;
440 fn deref(&self) -> &Self::Target {self.buffer.as_ref().unwrap()}
441}
442impl<'s> DerefMut for Line<'s> {
443 fn deref_mut(&mut self) -> &mut Self::Target {self.buffer.as_mut().unwrap()}
444}
445impl<'s> Write for Line<'s> {
446 fn write(&mut self, buf: &[u8]) -> IoResult<usize> {
447 self.inner.write(buf)
448 }
449 fn flush(&mut self) -> IoResult<()> {
450 self.inner.flush()
451 }
452}
453
454impl<'s> EmitEscapes for Line<'s> {
455 fn escapes_recognized(&self) -> bool {self.should_color}
456}
457
458
459
460/*
461 * NO NEW LINE
462 */
463
464impl<'s> NoNewLine<'s> {
465 /// Print this `NoNewLine` into the [`Stream`]. This is equivalent to dropping.
466 pub fn print(self) {}
467 /// Discard and drop this buffer without printing it
468 ///
469 /// This also elides flushing the [`Stream`].
470 pub fn discard(mut self) {self.buffer.take();}
471}
472
473impl<'s> Drop for NoNewLine<'s> {
474 /// The `NoNewLine` prints itself to the [`Stream`] when dropped
475 ///
476 /// This flushes the output stream manually because it cannot rely on the newline triggering "automatic" flushing.
477 fn drop(&mut self) {
478 if let Some(buffer) = self.buffer.as_mut() {
479 self.stream.print_buffer_and_flush(buffer).unwrap();
480 }
481 }
482}
483impl<'s> Deref for NoNewLine<'s> {
484 type Target = Buffer;
485 fn deref(&self) -> &Self::Target {self.buffer.as_ref().unwrap()}
486}
487impl<'s> DerefMut for NoNewLine<'s> {
488 fn deref_mut(&mut self) -> &mut Self::Target {self.buffer.as_mut().unwrap()}
489}
490
491
492/*
493 * INTIAL CONTENT
494 */
495
496/// Provide no initial content: `con.status(..)`
497///
498/// This is unidiomatic but I don't like `con.status(())`
499impl InitialContent for std::ops::RangeFull {
500 fn init_buffer(&self, _buf: &mut Buffer) {}
501}
502
503/// Provide no initial content: `con.status(())`
504impl InitialContent for () {
505 fn init_buffer(&self, _buf: &mut Buffer) {}
506}
507
508/// Provide no initial content: `con.status(None)`
509impl InitialContent for Option<()> {
510 fn init_buffer(&self, _buf: &mut Buffer) {}
511}
512
513
514/// Initialize the buffer with a string literal: `con.status("Test123 :^)")`
515impl InitialContent for &str {
516 fn init_buffer(&self, buf: &mut Buffer) {
517 buf.push_plain(self);
518 }
519}
520
521/// Initialize the buffer with a [`String`]: `con.status(format!("Test123 :^)"))`
522impl InitialContent for String {
523 fn init_buffer(&self, buf: &mut Buffer) {
524 buf.push_plain(self);
525 }
526}
527
528/// Initialize the buffer with [`format_args!`]: just as convenient as [`format!`] but without allocating
529///
530/// `con.status(format_args!("Test123 :^)"))`
531impl InitialContent for Arguments<'_> {
532 fn init_buffer(&self, buf: &mut Buffer) {
533 buf.push_plain(self);
534 }
535}
536
537/// Initialize with a [`Inline`] implementor
538impl<T: Inline + ?Sized> InitialContent for T {
539 fn init_buffer(&self, buf: &mut Buffer) {self.inline(buf)}
540}