Skip to main content

fyi_msg/msg/
kind.rs

1/*!
2# FYI Msg: Message Kinds (Prefixes).
3*/
4
5use crate::{
6	AnsiColor,
7	Msg,
8};
9use fyi_ansi::ansi;
10use std::{
11	borrow::Cow,
12	fmt,
13};
14
15
16
17/// # Helper: `MsgKind` Setup.
18macro_rules! msg_kind {
19	// A neat counting trick adapted from The Little Book of Rust Macros, used
20	// here to figure out the size of the ALL array.
21	(@count $odd:tt) => ( 1 );
22	(@count $odd:tt $( $a:tt $b:tt )+) => ( (msg_kind!(@count $($a)+) * 2) + 1 );
23	(@count $( $a:tt $b:tt )+) =>         (  msg_kind!(@count $($a)+) * 2      );
24
25	// Define MsgKind, MsgKind::ALL, and MsgKind::as_str_prefix.
26	(@build $( $k:ident $v:expr, )+) => (
27		#[expect(missing_docs, reason = "Redudant.")]
28		#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
29		/// # Message Kind.
30		///
31		/// This enum contains built-in prefixes for [`Msg`](crate::Msg). These are
32		/// generally only used to initiate a new message with this prefix, like:
33		///
34		/// ## Examples
35		///
36		/// ```
37		/// use fyi_msg::{Msg, MsgKind};
38		///
39		/// // Error: Oh no!
40		/// assert_eq!(
41		///     Msg::new(MsgKind::Error, "Oh no!"),
42		///     MsgKind::Error.into_msg("Oh no!"),
43		/// );
44		/// ```
45		///
46		/// Most kinds have their own dedicated [`Msg`] helper method which, unlike the
47		/// previous examples, comes with a line break at the end.
48		///
49		/// ```
50		/// use fyi_msg::{Msg, MsgKind};
51		///
52		/// // Error: Oh no!\n
53		/// assert_eq!(
54		///     Msg::error("Oh no!"),
55		///     Msg::new(MsgKind::Error, "Oh no!").with_newline(true),
56		/// );
57		/// ```
58		pub enum MsgKind {
59			$( $k, )+
60		}
61
62		impl MsgKind {
63			/// # All Variants.
64			///
65			/// This array can be used to cheaply iterate through all message kinds.
66			pub const ALL: [Self; msg_kind!(@count $($k)+)] = [
67				$( Self::$k, )+
68			];
69
70			#[inline]
71			#[must_use]
72			/// # As String Slice (Prefix).
73			///
74			/// Return the kind as a string slice, formatted and with a trailing `": "`,
75			/// same as [`Msg`] uses for prefixes.
76			pub(crate) const fn as_str_prefix(self) -> &'static str {
77				match self {
78					$( Self::$k => $v, )+
79				}
80			}
81		}
82	);
83
84	// Define the one-shot Msg helpers.
85	(@msg $( $k:ident $fn:ident $v:expr, )+) => (
86		/// ## [`MsgKind`] One-Shots.
87		impl Msg {
88			$(
89				#[must_use]
90				#[doc = concat!("# New ", stringify!($k), ".")]
91				///
92				#[doc = concat!("Create a new [`Msg`] with a built-in [`MsgKind::", stringify!($k), "`] prefix _and_ trailing line break.")]
93				///
94				/// ## Examples.
95				///
96				/// ```
97				/// use fyi_msg::{Msg, MsgKind};
98				///
99				/// assert_eq!(
100				#[doc = concat!("    Msg::", stringify!($fn), "(\"Hello World\"),")]
101				#[doc = concat!("    Msg::new(MsgKind::", stringify!($k), ", \"Hello World\").with_newline(true),")]
102				/// );
103				/// ```
104				pub fn $fn<S: AsRef<str>>(msg: S) -> Self {
105					// Glue it all together.
106					let msg = msg.as_ref();
107					let m_end = $v.len() + msg.len();
108					let mut inner = String::with_capacity(m_end + 1);
109					inner.push_str($v);
110					inner.push_str(msg);
111					inner.push('\n');
112
113					// Done!
114					Self {
115						inner,
116						toc: super::toc!($v.len(), m_end, true),
117					}
118				}
119			)+
120		}
121	);
122
123	// Generate an ANSI-formatted Msg prefix for a given kind.
124	(@prefix $kind:ident $color:tt) => (
125		concat!(ansi!((bold, $color) stringify!($kind), ":"), " ")
126	);
127
128	// Entry point!
129	($( $kind:ident $fn:ident $str:literal $color:tt $color_ident:ident, )+) => (
130		#[cfg(feature = "bin_kinds")]
131		msg_kind!{
132			@build
133			None "",
134			Confirm msg_kind!(@prefix Confirm dark_orange),
135			$( $kind msg_kind!(@prefix $kind $color), )+
136			Blank "",
137			Custom "",
138		}
139
140		#[cfg(not(feature = "bin_kinds"))]
141		msg_kind!{
142			@build
143			None "",
144			Confirm msg_kind!(@prefix Confirm dark_orange),
145			$( $kind msg_kind!(@prefix $kind $color), )+
146		}
147
148		msg_kind!{
149			@msg
150			$( $kind $fn msg_kind!(@prefix $kind $color), )+
151		}
152
153		impl MsgKind {
154			#[must_use]
155			/// # As String Slice.
156			///
157			/// Return the kind as a string slice, _without_ the formatting and trailing
158			/// `": "` used by [`Msg`].
159			///
160			/// ## Examples
161			///
162			/// ```
163			/// use fyi_msg::MsgKind;
164			///
165			/// assert_eq!(MsgKind::Error.as_str(), "Error");
166			/// assert_eq!(MsgKind::Success.as_str(), "Success");
167			///
168			/// // Note that None is empty.
169			/// assert_eq!(MsgKind::None.as_str(), "");
170			/// ```
171			pub const fn as_str(self) -> &'static str {
172				match self {
173					Self::Confirm => "Confirm",
174					$( Self::$kind => stringify!($kind), )+
175					_ => "",
176				}
177			}
178
179			#[cfg(feature = "bin_kinds")]
180			#[doc(hidden)]
181			#[must_use]
182			/// # Command.
183			///
184			/// Return the corresponding CLI (sub)command that triggers this kind.
185			///
186			/// Note: this is only intended for use by the `fyi` binary; the method
187			/// may change without warning.
188			pub const fn command(self) -> &'static str {
189				match self {
190					Self::Blank => "blank",
191					Self::Confirm => "confirm",
192					Self::Custom => "print",
193					Self::None => "",
194					$( Self::$kind => stringify!($fn), )+
195				}
196			}
197
198			#[must_use]
199			/// # Prefix Color.
200			///
201			/// Return the color used by this kind when playing the role of a [`Msg`]
202			/// prefix, or `None` if [`MsgKind::None`], which has neither content nor
203			/// formatting.
204			///
205			/// ## Examples
206			///
207			/// ```
208			/// use fyi_msg::{AnsiColor, MsgKind};
209			///
210			/// assert_eq!(
211			///     MsgKind::Info.prefix_color(),
212			///     Some(AnsiColor::LightMagenta),
213			/// );
214			/// ```
215			pub const fn prefix_color(self) -> Option<AnsiColor> {
216				match self {
217					#[cfg(feature = "bin_kinds")] Self::None | Self::Blank | Self::Custom => None,
218					#[cfg(not(feature = "bin_kinds"))] Self::None => None,
219					Self::Confirm =>  Some(AnsiColor::DarkOrange),
220					$( Self::$kind => Some(AnsiColor::$color_ident), )+
221				}
222			}
223		}
224
225		#[expect(clippy::string_lit_as_bytes, reason = "We need to test equality.")]
226		/// # Command Sanity Check.
227		///
228		/// We had to manually duplicate some values to work around macro
229		/// limitations. Let's make sure we didn't mess anything up!
230		const _: () = {
231			$(
232				assert!(
233					stringify!($fn).len() == $str.len(),
234					"BUG: Function/string/bytes are not equal.",
235				);
236				let b1 = stringify!($fn).as_bytes();
237				let b2 = $str.as_bytes();
238				let mut i = 0;
239				while i < b1.len() {
240					assert!(
241						b1[i] == b2[i],
242						"BUG: Function/string/bytes are not equal.",
243					);
244					i += 1;
245				}
246			)+
247		};
248
249		#[cfg(feature = "bin_kinds")]
250		argyle::argue! {
251			/// # CLI Command Arguments.
252			pub CliCommandArg,
253
254			/// # CLI Command Argument Iterator.
255			pub CliCommandArgIter,
256
257			// Commands first, then version.
258			Blank   "blank",
259			Confirm "confirm" "prompt",
260			Custom  "print",
261			$( $kind $str, )+
262			Version "-V"      "--version" "version",
263		}
264	);
265}
266
267msg_kind! {
268	Aborted  aborted  "aborted"  light_red     LightRed,
269	Crunched crunched "crunched" light_green   LightGreen,
270	Debug    debug    "debug"    light_cyan    LightCyan,
271	Done     done     "done"     light_green   LightGreen,
272	Error    error    "error"    light_red     LightRed,
273	Found    found    "found"    light_green   LightGreen,
274	Info     info     "info"     light_magenta LightMagenta,
275	Notice   notice   "notice"   light_magenta LightMagenta,
276	Review   review   "review"   light_cyan    LightCyan,
277	Skipped  skipped  "skipped"  light_yellow  LightYellow,
278	Success  success  "success"  light_green   LightGreen,
279	Task     task     "task"     199           Misc199,
280	Warning  warning  "warning"  light_yellow  LightYellow,
281}
282
283impl Default for MsgKind {
284	#[inline]
285	fn default() -> Self { Self::None }
286}
287
288impl fmt::Display for MsgKind {
289	#[inline]
290	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291		<str as fmt::Display>::fmt(self.as_str(), f)
292	}
293}
294
295/// ## Details.
296impl MsgKind {
297	#[cfg(feature = "bin_kinds")]
298	#[must_use]
299	/// # Is Empty.
300	///
301	/// This returns `true` for [`MsgKind::None`], [`MsgKind::Blank`], and
302	/// [`MsgKind::Custom`], `false` for everything else.
303	pub const fn is_empty(self) -> bool {
304		matches!(self, Self::None | Self::Blank | Self::Custom)
305	}
306
307	#[cfg(not(feature = "bin_kinds"))]
308	#[must_use]
309	/// # Is Empty.
310	///
311	/// This returns `true` for [`MsgKind::None`], `false` for everything else.
312	pub const fn is_empty(self) -> bool { matches!(self, Self::None) }
313
314	#[inline]
315	#[must_use]
316	/// # Into Message.
317	///
318	/// This is a convenience method to generate a new message using this
319	/// prefix, equivalent to passing the kind to [`Msg::new`] manually.
320	///
321	/// ## Examples
322	///
323	/// ```
324	/// use fyi_msg::{Msg, MsgKind};
325	///
326	/// assert_eq!(
327	///     MsgKind::Error.into_msg("Oops"),
328	///     Msg::new(MsgKind::Error, "Oops"),
329	/// );
330	/// ```
331	///
332	/// Most kinds — everything but [`MsgKind::None`] and [`MsgKind::Confirm`] —
333	/// have same-named shorthand methods on the `Msg` struct itself that work
334	/// like the above, except they also add a line break to the end.
335	///
336	/// ```
337	/// use fyi_msg::{Msg, MsgKind};
338	///
339	/// assert_eq!(
340	///     Msg::error("Oops"),
341	///     MsgKind::Error.into_msg("Oops").with_newline(true),
342	/// );
343	/// ```
344	pub fn into_msg<S>(self, msg: S) -> Msg
345	where S: AsRef<str> { Msg::new(self, msg) }
346}
347
348
349
350/// # Into Message Prefix.
351///
352/// This trait provides everything necessary to format prefixes passed to
353/// [`Msg::new`], [`Msg::set_prefix`], and [`Msg::with_prefix`].
354///
355/// More specifically, it allows users to choose between the "easy" built-in
356/// [`MsgKind`] prefixes and custom ones, with or without ANSI formatting.
357///
358/// Custom prefixes can be any of the usual string types — `&str`,
359/// `String`/`&String`, or `Cow<str>`/`&Cow<str>` — optionally tupled with an
360/// [`AnsiColor`] for formatting.
361///
362/// See [`Msg::new`] for more details.
363pub trait IntoMsgPrefix {
364	/// # Prefix Length.
365	///
366	/// Returns the total byte length of the fully-rendered prefix, including
367	/// any ANSI sequences and trailing `": "` separator.
368	fn prefix_len(&self) -> usize;
369
370	/// # Push Prefix.
371	///
372	/// Push the complete prefix to an existing string.
373	fn prefix_push(&self, dst: &mut String);
374
375	#[inline]
376	/// # Prefix String.
377	///
378	/// Returns the complete prefix for rendering.
379	///
380	/// [`MsgKind`] prefixes are static and require no allocation, but custom
381	/// types (unless empty) do to join all the pieces together.
382	fn prefix_str(&self) -> Cow<'_, str> {
383		let mut out = String::with_capacity(self.prefix_len());
384		self.prefix_push(&mut out);
385		Cow::Owned(out)
386	}
387}
388
389impl IntoMsgPrefix for MsgKind {
390	#[inline]
391	/// # Prefix Length.
392	fn prefix_len(&self) -> usize { self.as_str_prefix().len() }
393
394	#[inline]
395	/// # Prefix String.
396	fn prefix_str(&self) -> Cow<'_, str> { Cow::Borrowed(self.as_str_prefix()) }
397
398	#[inline]
399	/// # Push Prefix.
400	fn prefix_push(&self, dst: &mut String) { dst.push_str(self.as_str_prefix()); }
401}
402
403/// # Helper: `IntoMsgPrefix`.
404macro_rules! into_prefix {
405	($($ty:ty),+) => ($(
406		impl IntoMsgPrefix for $ty {
407			#[inline]
408			/// # Prefix Length.
409			fn prefix_len(&self) -> usize {
410				let len = self.len();
411				if len == 0 { 0 }
412				else { len + 2 } // For the ": " separator.
413			}
414
415			#[inline]
416			/// # Push Prefix.
417			fn prefix_push(&self, dst: &mut String) {
418				if ! self.is_empty() {
419					dst.push_str(self);
420					dst.push_str(": ");
421				}
422			}
423		}
424
425		impl IntoMsgPrefix for ($ty, AnsiColor) {
426			#[inline]
427			/// # Prefix Length.
428			fn prefix_len(&self) -> usize {
429				let len = self.0.len();
430				if len == 0 { 0 }
431				else {
432					self.1.as_str_bold().len() + self.0.len() +
433					AnsiColor::RESET_PREFIX.len()
434				}
435			}
436
437			#[inline]
438			/// # Push Prefix.
439			fn prefix_push(&self, dst: &mut String) {
440				if ! self.0.is_empty() {
441					dst.push_str(self.1.as_str_bold());
442					dst.push_str(&self.0);
443					dst.push_str(AnsiColor::RESET_PREFIX);
444				}
445			}
446		}
447
448		impl IntoMsgPrefix for ($ty, u8) {
449			#[inline]
450			/// # Prefix Length.
451			fn prefix_len(&self) -> usize {
452				let len = self.0.len();
453				if len == 0 { 0 }
454				else {
455					let color = AnsiColor::from_u8(self.1);
456					color.as_str_bold().len() + self.0.len() +
457					AnsiColor::RESET_PREFIX.len()
458				}
459			}
460
461			#[inline]
462			/// # Push Prefix.
463			fn prefix_push(&self, dst: &mut String) {
464				if ! self.0.is_empty() {
465					dst.push_str(AnsiColor::from_u8(self.1).as_str_bold());
466					dst.push_str(&self.0);
467					dst.push_str(AnsiColor::RESET_PREFIX);
468				}
469			}
470		}
471	)+);
472}
473into_prefix!(&str, &String, String, &Cow<'_, str>, Cow<'_, str>);
474
475
476
477#[cfg(test)]
478mod test {
479	use super::*;
480
481	#[test]
482	fn t_as_str_prefix() {
483		// Make sure our hardcoded prefix strings look like they're supposed
484		// to, using the ansi builder/color as an alternative.
485		for kind in MsgKind::ALL {
486			// Not all kinds have formatted versions.
487			let Some(color) = kind.prefix_color() else { continue; };
488			let manual = format!("{}{kind}:{} ", color.as_str_bold(), AnsiColor::RESET);
489
490			assert_eq!(manual, kind.as_str_prefix());
491		}
492	}
493}