farben_core/registry.rs
1//! Global named style registry.
2//!
3//! Stores user-defined [`Style`] values under string keys, allowing markup like
4//! `[danger]` to expand into a pre-configured combination of colors and emphasis.
5//! Styles are registered at startup via the [`style!`] macro and optionally given
6//! a prefix string via the [`prefix!`] macro.
7//!
8//! The registry is process-global and backed by a [`Mutex`]-protected [`HashMap`].
9//! All operations are safe to call from multiple threads, though the typical usage
10//! pattern is to register styles once at program startup.
11
12use std::{
13 collections::HashMap,
14 sync::{Mutex, OnceLock},
15};
16
17use crate::ansi::Style;
18use crate::errors::LexError;
19
20static REGISTRY: OnceLock<Mutex<HashMap<String, Style>>> = OnceLock::new();
21
22/// Registers a named style in the global registry.
23///
24/// If a style with `name` already exists, it is replaced.
25pub fn insert_style(name: impl Into<String>, style: Style) {
26 REGISTRY
27 .get_or_init(|| Mutex::new(HashMap::new()))
28 .lock()
29 .unwrap()
30 .insert(name.into(), style);
31}
32
33/// Sets the prefix string for an already-registered named style.
34///
35/// The prefix is prepended to the style's escape sequence at render time,
36/// allowing a named style to inject arbitrary text before its ANSI codes.
37///
38/// # Panics
39///
40/// Panics if `name` has not been registered via [`insert_style`] (or the `style!` macro).
41pub fn set_prefix(name: impl Into<String>, prefix: impl Into<String>) {
42 let name = name.into();
43 let prefix = prefix.into();
44
45 let mut map = REGISTRY
46 .get_or_init(|| Mutex::new(HashMap::new()))
47 .lock()
48 .unwrap();
49 if let Some(style) = map.get_mut(&name) {
50 style.prefix = Some(prefix);
51 } else {
52 panic!("prefix!() called with unknown style '{name}', define it with style!() first");
53 }
54}
55
56/// Looks up a named style in the global registry.
57///
58/// # Errors
59///
60/// Returns `LexError::InvalidTag` if `query` does not match any registered style name.
61pub(crate) fn search_registry(query: impl Into<String>) -> Result<Style, LexError> {
62 let map = REGISTRY
63 .get_or_init(|| Mutex::new(HashMap::new()))
64 .lock()
65 .unwrap();
66
67 let query = query.into();
68 match map.get(&query) {
69 Some(style) => Ok(style.clone()),
70 None => Err(LexError::InvalidTag(query)),
71 }
72}
73
74/// Defines a named style in the global registry.
75///
76/// Parses `$markup` as a farben markup string and stores the resulting [`Style`]
77/// under `$name`. Panics if the markup is invalid.
78///
79/// # Example
80///
81/// ```ignore
82/// style!("danger", "[bold red]");
83/// // [danger] in markup now expands to bold red text
84/// ```
85#[macro_export]
86macro_rules! style {
87 ($name:expr, $markup:expr) => {
88 farben_core::registry::insert_style(
89 $name,
90 farben_core::ansi::Style::parse($markup).unwrap(),
91 );
92 };
93}
94
95/// Sets a prefix string on a previously defined named style.
96///
97/// The prefix is injected as a literal string before the style's ANSI escape sequence
98/// when rendered. The style must already exist in the registry; call [`style!`] first.
99///
100/// # Panics
101///
102/// Panics if `$name` has not been registered.
103///
104/// # Example
105///
106/// ```ignore
107/// style!("warn", "[yellow]");
108/// prefix!("warn", "⚠ ");
109/// // [warn] now renders "⚠ " followed by the yellow escape sequence
110/// ```
111#[macro_export]
112macro_rules! prefix {
113 ($name:expr, $prefix:expr) => {
114 farben_core::registry::set_prefix($name, $prefix);
115 };
116}