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}