okansi 0.3.0

An ok library for creating ANSI terminal styles
Documentation
use std::fmt::{Display, Formatter, Write};

use super::{AnsiColor, codes, codes::bitmasks};

/// Contains data and methods for easily configuring ANSI format sequences
pub struct AnsiStyle {
	pub foreground: Option<AnsiColor>,
	pub background: Option<AnsiColor>,
	pub flags: u8,
}
impl AnsiStyle {
	fn set_flags(&mut self, mask: u8, enabled: bool) -> &mut Self {
		if enabled {
			self.flags |= mask;
		} else {
			self.flags &= !mask;
		}
		self
	}
	fn chain_set_flags(mut self, mask: u8, enabled: bool) -> Self {
		self.set_flags(mask, enabled);
		self
	}

	/// Returns a new plain `AnsiStyle`
	pub const fn plain() -> Self {
		Self {
			foreground: None,
			background: None,
			flags: 0,
		}
	}
	pub const fn with_background(background: AnsiColor) -> Self {
		Self {
			foreground: None,
			background: Some(background),
			flags: 0,
		}
	}
	pub const fn with_foreground(foreground: AnsiColor) -> Self {
		Self {
			foreground: Some(foreground),
			background: None,
			flags: 0,
		}
	}

	/// Returns whether the bold flag is set
	pub fn is_bold(&self) -> bool {
		self.flags & bitmasks::BOLD == bitmasks::BOLD
	}
	/// Returns whether the italic flag is set
	pub fn is_italic(&self) -> bool {
		self.flags & bitmasks::ITALIC == bitmasks::ITALIC
	}
	/// Returns whether the underline flag is set
	pub fn is_underline(&self) -> bool {
		self.flags & bitmasks::UNDERLINE == bitmasks::UNDERLINE
	}
	/// Returns whether the strikethough flag is set
	pub fn is_strikethrough(&self) -> bool {
		self.flags & bitmasks::STRIKETHROUGH == bitmasks::STRIKETHROUGH
	}
	/// Returns whether the dim flag is set
	pub fn is_dim(&self) -> bool {
		self.flags & bitmasks::DIM == bitmasks::DIM
	}
	/// Returns whether the blinking flag is set
	pub fn is_blinking(&self) -> bool {
		self.flags & bitmasks::BLINKING == bitmasks::BLINKING
	}
	/// Returns whether the reverse flag is set
	pub fn is_reverse(&self) -> bool {
		self.flags & bitmasks::REVERSE == bitmasks::REVERSE
	}
	/// Returns whether the hidden flag is set
	pub fn is_hidden(&self) -> bool {
		self.flags & bitmasks::HIDDEN == bitmasks::HIDDEN
	}

	/// Returns whether the `AnsiStyle` will not be applied
	pub fn is_plain(&self) -> bool {
		!((self.flags != 0) | self.foreground.is_some() | self.background.is_some())
	}

	/// Modifies the `AnsiStyle`'s bold flag, must be mutable
	pub fn set_bold(&mut self, enabled: bool) -> &mut Self {
		self.set_flags(bitmasks::BOLD, enabled)
	}
	/// Modifies the `AnsiStyle`'s italic flag, must be mutable
	pub fn set_italic(&mut self, enabled: bool) -> &mut Self {
		self.set_flags(bitmasks::ITALIC, enabled)
	}
	/// Modifies the `AnsiStyle`'s underline flag, must be mutable
	pub fn set_underline(&mut self, enabled: bool) -> &mut Self {
		self.set_flags(bitmasks::UNDERLINE, enabled)
	}
	/// Modifies the `AnsiStyle`'s strikethrough flag, must be mutable
	pub fn set_strikethrough(&mut self, enabled: bool) -> &mut Self {
		self.set_flags(bitmasks::STRIKETHROUGH, enabled)
	}
	/// Modifies the `AnsiStyle`'s dim flag, must be mutable
	pub fn set_dim(&mut self, enabled: bool) -> &mut Self {
		self.set_flags(bitmasks::DIM, enabled)
	}
	/// Modifies the `AnsiStyle`'s blinking flag, must be mutable
	pub fn set_blinking(&mut self, enabled: bool) -> &mut Self {
		self.set_flags(bitmasks::BLINKING, enabled)
	}
	/// Modifies the `AnsiStyle`'s reverse flag, must be mutable
	pub fn set_reverse(&mut self, enabled: bool) -> &mut Self {
		self.set_flags(bitmasks::REVERSE, enabled)
	}
	/// Modifies the `AnsiStyle`'s hidden flag, must be mutable
	pub fn set_hidden(&mut self, enabled: bool) -> &mut Self {
		self.set_flags(bitmasks::HIDDEN, enabled)
	}
	/// Modifies the `AnsiStyle`'s foreground color, must be mutable
	pub fn set_fg(&mut self, foreground: Option<AnsiColor>) -> &mut Self {
		self.foreground = foreground;
		self
	}
	/// Modifies the `AnsiStyle`'s background color, must be mutable
	pub fn set_bg(&mut self, background: Option<AnsiColor>) -> &mut Self {
		self.background = background;
		self
	}

	/// Chaining variant of `AnsiStyle.set_bold`
	pub fn chain_bold(self, enabled: bool) -> Self {
		self.chain_set_flags(bitmasks::BOLD, enabled)
	}
	/// Chaining variant of `AnsiStyle.set_italic`
	pub fn chain_italic(self, enabled: bool) -> Self {
		self.chain_set_flags(bitmasks::ITALIC, enabled)
	}
	/// Chaining variant of `AnsiStyle.set_underline`
	pub fn chain_underline(self, enabled: bool) -> Self {
		self.chain_set_flags(bitmasks::UNDERLINE, enabled)
	}
	/// Chaining variant of `AnsiStyle.set_strikethrough`
	pub fn chain_strikethrough(self, enabled: bool) -> Self {
		self.chain_set_flags(bitmasks::STRIKETHROUGH, enabled)
	}
	/// Chaining variant of `AnsiStyle.set_dim`
	pub fn chain_dim(self, enabled: bool) -> Self {
		self.chain_set_flags(bitmasks::DIM, enabled)
	}
	/// Chaining variant of `AnsiStyle.set_blinking`
	pub fn chain_blinking(self, enabled: bool) -> Self {
		self.chain_set_flags(bitmasks::BLINKING, enabled)
	}
	/// Chaining variant of `AnsiStyle.set_reverse`
	pub fn chain_reverse(self, enabled: bool) -> Self {
		self.chain_set_flags(bitmasks::REVERSE, enabled)
	}
	/// Chaining variant of `AnsiStyle.set_hidden`
	pub fn chain_hidden(self, enabled: bool) -> Self {
		self.chain_set_flags(bitmasks::HIDDEN, enabled)
	}
	/// Chaining variant of `AnsiStyle.set_fg`
	pub fn chain_fg(mut self, foreground: Option<AnsiColor>) -> Self {
		self.foreground = foreground;
		self
	}
	/// Chaining variant of `AnsiStyle.set_bg`
	pub fn chain_bg(mut self, background: Option<AnsiColor>) -> Self {
		self.background = background;
		self
	}

	/// Modifies the passed `String` by applying the `AnsiStyle`
	pub fn mutate_apply(&self, text: &mut String) {
		text.insert_str(0, &self.to_string());
	}
	/// Modifies the passed `String` by applying the `AnsiStyle` with an `\x1b[0m` suffix
	pub fn mutate_apply_with_reset(&self, text: &mut String) {
		if self.is_plain() {
			return;
		}
		self.mutate_apply(text);
		text.push_str(codes::RESET);
	}

	/// Returns a cloned `String` of the passed in text with the `AnsiStyle` applied
	pub fn apply(&self, text: &str) -> String {
		let mut new_text = text.to_owned();
		self.mutate_apply(&mut new_text);
		new_text
	}
	/// Returns a cloned `String` of the passed in text with the `AnsiStyle` applied and an `\x1b[0m` suffix
	pub fn apply_with_reset(&self, text: &str) -> String {
		let mut new_text = text.to_owned();
		self.mutate_apply_with_reset(&mut new_text);
		new_text
	}

	// Directly writes this `AnsiStyle`'s prefix to a stream.
	pub fn write_to_stream<W: Write>(&self, stream_out: &mut W) -> std::fmt::Result {
		if self.is_plain() {
			return Ok(());
		}

		stream_out.write_str("\x1b[")?;

		let mut wrote = false;
		let mut append = |arg: &str| -> std::fmt::Result {
			if wrote {
				stream_out.write_char(';')?;
			}
			stream_out.write_str(arg)?;
			wrote = true;
			Ok(())
		};

		if self.is_bold() {
			append("1")?
		}
		if self.is_dim() {
			append("2")?
		}
		if self.is_italic() {
			append("3")?
		}
		if self.is_underline() {
			append("4")?
		}
		if self.is_blinking() {
			append("5")?
		}
		if self.is_reverse() {
			append("7")?
		}
		if self.is_hidden() {
			append("8")?
		}
		if self.is_strikethrough() {
			append("9")?
		}

		if let Some(foreground) = &self.foreground {
			append(foreground.gen_sequence(false).as_str())?
		}
		if let Some(background) = &self.background {
			append(background.gen_sequence(true).as_str())?
		}

		stream_out.write_char('m')?;

		Ok(())
	}
}
impl Display for AnsiStyle {
	fn fmt(&self, handle: &mut Formatter) -> std::fmt::Result {
		self.write_to_stream(handle)
	}
}

#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn new_style_is_plain() {
		let style = AnsiStyle::plain();

		assert!(style.is_plain(), "Not plain")
	}

	#[test]
	fn flag_positions() {
		let style = AnsiStyle {
			flags: 0b101,
			background: None,
			foreground: None,
		};

		assert!(style.is_bold(), "Style should be bold");
		assert!(!style.is_italic(), "Style shouldn't be italic");
		assert!(style.is_underline(), "Style should be underlined");
		assert!(
			!style.is_strikethrough(),
			"Style shouldn't be strikethrough"
		);
	}
	#[test]
	fn mutate_flags() {
		let mut style = AnsiStyle::plain();
		assert_eq!(style.flags, 0, "flags should be 0");

		style.set_bold(true);
		assert_eq!(style.flags, 1, "flags should be 1");

		style.set_italic(true);
		assert_eq!(style.flags, 3, "flags should be 3");

		style.set_underline(true);
		assert_eq!(style.flags, 7, "flags should be 7");

		style.set_strikethrough(true);
		assert_eq!(style.flags, 15, "flags should be 15");
	}

	#[test]
	fn plain_mutate_apply_should_not_modify() {
		let plain_style = AnsiStyle::plain();
		let mut text = String::from("Hello world!");

		plain_style.mutate_apply(&mut text);

		assert_eq!(
			&text[..],
			"Hello world!",
			"String was modified by a plain style, got {text}"
		);

		plain_style.mutate_apply_with_reset(&mut text);

		assert_eq!(
			&text[..],
			"Hello world!",
			"String was modified by a plain style, got {text}"
		)
	}
	#[test]
	fn mutate_apply_modify() {
		let style = AnsiStyle {
			flags: 0b1,
			background: None,
			foreground: None,
		};
		let mut text = String::from("Hello world!");
		let expected = "\x1b[1mHello world!";

		assert_eq!(
			&text[..],
			"Hello world!" // "String doesn't equal Hello world?"
		);

		style.mutate_apply(&mut text);

		assert_eq!(
			&text[..],
			expected,
			"Expected: {:3?}\nGot: {:3?}",
			expected.as_bytes(),
			text.as_bytes()
		);
	}

	#[test]
	fn plain_apply_should_not_differ() {
		let style = AnsiStyle::plain();
		let input_text = String::from("Hello world!");

		assert_eq!(
			&input_text,
			"Hello world!" // "String doesn't equal Hello world?"
		);

		let output_text = style.apply(&input_text);

		assert_eq!(
			output_text,
			input_text // "Output text doesn't match input text"
		)
	}
}