use directories::ProjectDirs;
use farben::prelude::*;
use farben_build::core::parse;
use rustyline::{Editor, error::ReadlineError, highlight::Highlighter};
use rustyline_derive::{Completer, Helper, Hinter, Validator};
use std::borrow::Cow;
use std::path::PathBuf;
use std::sync::Mutex;
use std::sync::OnceLock;
static USER_STYLES: OnceLock<Mutex<Vec<String>>> = OnceLock::new();
fn user_styles() -> &'static Mutex<Vec<String>> {
USER_STYLES.get_or_init(|| Mutex::new(Vec::new()))
}
fn config_dir() -> Option<PathBuf> {
ProjectDirs::from("", "", "thuli").map(|d| d.config_dir().to_path_buf())
}
fn load_user_theme_styles() {
let Some(dir) = config_dir() else { return };
let path = dir.join("theme.frb.toml");
let Ok(contents) = std::fs::read_to_string(&path) else {
return;
};
let config = match parse(&contents) {
Ok(c) => c,
Err(e) => {
ceprintln!("[thuli::error][/]: failed to parse theme file");
eprintln!("{}: {e}", path.display());
return;
}
};
for (name, markup) in &config.styles {
if !name.starts_with("thuli::") {
ceprint!("[thuli::error][/]: ");
eprintln!("style '{name}' cannot be set in theme.frb.toml");
eprintln!(" theme.frb.toml is for thuli's own appearance");
eprintln!(" for project styles, use /load <file>");
continue;
}
match farben::Style::parse(markup) {
Ok(style) => insert_style(name, style),
Err(e) => {
ceprint!("[thuli::error][/]: in theme file, style '");
eprint!("{name}': ");
eprintln!("{e}");
}
}
}
}
fn load_user_theme_prefixes() {
let Some(dir) = config_dir() else { return };
let path = dir.join("theme.frb.toml");
let Ok(contents) = std::fs::read_to_string(&path) else {
return;
};
let Ok(config) = parse(&contents) else { return };
const ALLOWED_PREFIX_OVERRIDES: &[&str] = &["thuli::error", "thuli::tip"];
for (name, prefix) in &config.prefixes {
if !ALLOWED_PREFIX_OVERRIDES.contains(&name.as_str()) {
ceprint!("[thuli::error][/]: ");
eprintln!("prefix '{name}' cannot be set in theme.frb.toml");
eprintln!(" only 'thuli::error' and 'thuli::tip' prefixes may be overridden");
eprintln!(" for other styles, use /prefix at runtime or /load <file>");
continue;
}
if let Err(e) = set_prefix(name, prefix) {
ceprint!("[thuli::error][/]: ");
eprintln!("in theme file, prefix '{name}': {e}");
}
}
}
#[derive(Completer, Helper, Hinter, Validator)]
struct ThuliHelper;
impl Highlighter for ThuliHelper {
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
&'s self,
_prompt: &'p str,
_default: bool,
) -> Cow<'b, str> {
init_styles();
Cow::Owned(try_color("[thuli::prompt]>>>[/] ").unwrap_or_else(|_| ">>> ".to_string()))
}
fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
if line.is_empty() {
return Cow::Borrowed(line);
}
let is_command = line.starts_with('/') && !line.starts_with("//");
let command_end = if is_command {
line.find(char::is_whitespace).unwrap_or(line.len())
} else {
0
};
let mut out = String::with_capacity(line.len() + 32);
let mut in_bracket = false;
let mut prev_char: Option<char> = None;
if is_command {
out.push_str("\x1b[34;1m");
}
for (i, c) in line.char_indices() {
if is_command && i == command_end {
out.push_str("\x1b[0m");
}
match c {
'[' if prev_char != Some('\\') && !in_bracket => {
out.push_str("\x1b[4m");
out.push(c);
in_bracket = true;
}
']' if in_bracket => {
out.push(c);
out.push_str("\x1b[24m");
in_bracket = false;
}
_ => out.push(c),
}
prev_char = Some(c);
}
if in_bracket {
out.push_str("\x1b[24m");
}
if is_command && command_end == line.len()
&& !out.ends_with("\x1b[0m") {
out.push_str("\x1b[0m");
}
Cow::Owned(out)
}
fn highlight_char(
&self,
_line: &str,
_pos: usize,
_forced: rustyline::highlight::CmdKind,
) -> bool {
true
}
}
fn init_styles() {
apply_default_styles();
load_user_theme_styles();
apply_default_prefixes();
load_user_theme_prefixes();
}
fn apply_default_styles() {
style!("thuli::brand", "[ansi(93)]");
style!("thuli::frb", "[cyan]");
style!("thuli::error", "[red]");
style!("thuli::tip", "[cyan]");
style!("thuli::header", "[bold]");
style!("thuli::command", "[cyan]");
style!("thuli::prompt", "[bold cyan]");
}
fn apply_default_prefixes() {
prefix!("thuli::error", "error");
prefix!("thuli::tip", "tip");
}
fn main() -> rustyline::Result<()> {
init_styles();
if let Some(arg) = std::env::args().nth(1) {
return one_shot(arg);
}
repl()
}
fn one_shot(arg: String) -> rustyline::Result<()> {
if arg == "--help" || arg == "-h" {
cprintln!("[bold thuli::brand]thuli[/] is an interactive repl, directly run with 'thuli'");
cprintln!("or pass one argument to directly style and output");
cprintln!();
cprintln!("[thuli::header underline]usage:[/] thuli \\[<text>]");
cprintln!(" \\[<text>] : The text to format, in Farben markup");
std::process::exit(0);
}
if arg == "--version" {
cprintln!(
"[thuli::brand]thuli[/] {}, previewing [thuli::frb]frb0.17[/]",
env!("CARGO_PKG_VERSION")
);
std::process::exit(0);
}
match try_color(&arg) {
Ok(rendered) => {
println!("{rendered}")
}
Err(e) => {
eprintln!("{}", e.display(&arg));
std::process::exit(1);
}
}
Ok(())
}
fn repl() -> rustyline::Result<()> {
let mut rl: Editor<ThuliHelper, _> = Editor::new()?;
rl.set_helper(Some(ThuliHelper));
let mut warned_about_ctrl_c = false;
cprintln!(
"[thuli::brand]thuli {}[/] · the official farben repl, previewing [thuli::frb]frb0.17[/]",
env!("CARGO_PKG_VERSION")
);
cprintln!(
"run [yellow]\\[/help][/], [yellow]\\[/copyright][/], [yellow]\\[/license][/] for more information, [yellow]\\[/quit][/] to exit\n"
);
loop {
match rl.readline("[F] ") {
Ok(line) => {
warned_about_ctrl_c = false;
if !line.trim().is_empty() {
let _ = rl.add_history_entry(line.as_str());
}
if line.starts_with('/') && !line.starts_with("//") {
handle_command(&line);
} else {
let line = match line.strip_prefix('/') {
Some(r) => r.to_string(),
None => line,
};
if line.is_empty() {
continue;
}
match try_color(&line) {
Ok(rendered) => println!(" {rendered}"),
Err(e) => {
print_err(e, &line);
}
}
}
}
Err(ReadlineError::Eof) => {
println!("make great things with farben <3");
break;
}
Err(ReadlineError::Interrupted) => {
if warned_about_ctrl_c {
println!(" Ctrl+C discards the line. To exit, use Ctrl+D.");
}
warned_about_ctrl_c = true;
continue;
}
Err(e) => return Err(e),
}
}
Ok(())
}
fn handle_command(line: &str) {
let rest = &line[1..];
let mut parts = rest.splitn(2, char::is_whitespace);
let cmd = parts.next().unwrap_or("");
let args = parts.next().unwrap_or("").trim();
match cmd {
"quit" | "q" | "exit" | "x" => {
println!("make great things with farben <3");
std::process::exit(0)
}
"help" | "h" | "?" => print_help(),
"clear" | "cls" => print!("\x1B[2J\x1B[1;1H"),
"copyright" => print_copyright(),
"license" => print_license(),
"tags" => print_tags(),
"theme" => cmd_theme(),
"style" => cmd_style(args),
"prefix" => cmd_prefix(args),
"show" => cmd_show(args),
"load" => cmd_load(args),
"" => eprintln!(" empty command. try /help."),
other => eprintln!(" unknown command: /{other}. try /help."),
}
}
fn print_err(e: LexError, args: &str) {
ceprint!("[thuli::error][/]: ");
eprintln!("{e}");
eprintln!("{}", e.display(args));
ceprint!("[thuli::tip][/]: ");
eprintln!("if you meant to print the brackets directly, escape them '\\['");
}
fn print_help() {
cprintln!(
" [thuli::brand]thuli[/] is an interactive repl for experimenting with farben markup"
);
cprintln!(" [thuli::header]commands:[/]");
cprintln!(" [thuli::command]/help[/] show this message");
cprintln!(" [thuli::command]/clear[/] clear the screen");
cprintln!(" [thuli::command]/copyright[/] show copyright mentions");
cprintln!(" [thuli::command]/license[/] show project license");
cprintln!(
" [thuli::command]/load <path>[/] load styles from a .frb.toml file"
);
cprintln!(" [thuli::command]/prefix <style> <prefix>[/] apply a prefix to a style");
cprintln!(" [thuli::command]/quit[/] exit thuli");
cprintln!(" [thuli::command]/style <name> <markup>[/] register a new style");
cprintln!(" [thuli::command]/tags[/] show all tags");
cprintln!(" [thuli::command]/theme[/] info about thuli's app theme");
cprintln!(" to pass '/' to [thuli::brand]thuli[/], write double '//'");
}
fn print_copyright() {
cprintln!("[yellow]Copyright (c) 2026 RazkarStudio.");
cprintbln!("[bright-yellow]All rights reserved.");
cprintbln!(" razkar on Codeberg");
cprintbln!(" razkar-studio on GitHub");
cprintbln!(" razkar-studio on Crates-io");
cprintln!();
}
fn print_license() {
cprintln!(
"[thuli::brand]thuli[/] is licensed under either of the [yellow]MIT License[/] or [blue]Apache License, Version 2.0[/], at your option."
);
cprintln!("View the repository for more details");
cprintln!();
}
fn print_tags() {
cprintln!(" [thuli::header]reserved:[/]");
cprintln!(" thuli::brand");
cprintln!(" thuli::frb");
cprintln!(" thuli::error");
cprintln!(" thuli::tip");
cprintln!(" thuli::header");
cprintln!(" thuli::command");
cprintln!(" thuli::prompt");
cprintln!(" [thuli::header]built-in:[/]");
cprintln!(" [dim]emphasis[/] bold, dim, italic, underline, blink, strikethrough");
cprintln!(
" [dim]extended[/] double-underline, rapid-blink, reverse, invisible, overline"
);
cprintln!(" [dim]colors[/] black, red, green, yellow, blue, magenta, cyan, white");
cprintln!(
" [dim]bright[/] bright-black, bright-red, bright-green, bright-yellow, bright-blue, bright-magenta, bright-cyan, bright-white"
);
cprintln!(" [dim]formats[/] rgb(r,g,b), ansi(n), fg:<color>, bg:<color>");
cprintln!(" [dim]resets[/] /, /<tag>");
let styles = user_styles().lock().unwrap();
cprintln!(" [bold]registered:[/]");
if styles.is_empty() {
cprintln!(" [dim](none yet, define with /style)[/]");
} else {
for style in styles.iter() {
cprintln!(" {style}")
}
}
}
fn parse_two(args: &str) -> (&str, &str) {
let mut parts = args.splitn(2, char::is_whitespace);
(
parts.next().unwrap_or("").trim(),
parts.next().unwrap_or("").trim(),
)
}
use farben::core::{tokenize, tokens_to_markup};
fn cmd_show(args: &str) {
if args.is_empty() {
ceprintln!(" [thuli::header]usage[/] /show <markup>");
return;
}
match try_color(args) {
Ok(ansi) => {
let tokens = tokenize(args).unwrap();
println!(" {ansi}");
println!(" \u{2192} tokens: {}", tokens_to_markup(&tokens));
println!(
" \u{2192} ansi: {}",
format!("{:?}", ansi).replace("\\u{1b}", "\\x1b")
);
}
Err(e) => {
print_err(e, args);
}
}
}
fn cmd_style(args: &str) {
let (name, markup) = parse_two(args);
if name.is_empty() || markup.is_empty() {
ceprintln!(" [thuli::header]usage[/] /style <name> <markup>");
return;
}
if name.starts_with("thuli::") {
ceprintln!(
"[thuli::error][/]: you cannot modify or add specific styles prefixed with 'thuli::' regardless of status."
);
return;
}
match farben::Style::parse(markup) {
Ok(style) => {
{
let mut styles = user_styles().lock().unwrap();
if !styles.iter().any(|s| s == name) {
styles.push(name.to_string());
}
}
insert_style(name, style);
println!(" \u{2192} {name} = {markup}");
}
Err(e) => {
print_err(e, markup);
}
}
}
fn cmd_prefix(args: &str) {
let (name, prefix) = parse_two(args);
if name.is_empty() || prefix.is_empty() {
ceprintbln!(" [thuli::header]usage[/] /prefix <style> <prefix>");
return;
}
if name.starts_with("thuli::") {
ceprintln!(
"[thuli::error][/]: you cannot modify or add specific styles prefixed with 'thuli::' regardless of status."
);
return;
}
match set_prefix(name, prefix) {
Ok(_) => println!(" \u{2192} {name}: {prefix}"),
Err(e) => ceprintln!("[thuli::error][/]: {e}"),
};
}
fn cmd_theme() {
cprintln!(" [thuli::header]thuli theme[/]");
match config_dir() {
Some(dir) => {
let path = dir.join("theme.frb.toml");
let exists = path.exists();
cprint!(" config: ");
println!("{}", path.display());
cprint!(" status: ");
if exists {
cprintln!("[green]loaded[/]");
} else {
cprintln!("[dim](no theme file, using defaults)[/]");
cprintln!(
" to customize: create the file above with [thuli::command]\\[styles][/] / [thuli::command]\\[prefixes][/] sections"
);
}
}
None => {
cprintln!(" [dim](could not determine config directory)[/]");
}
}
}
fn cmd_load(args: &str) {
let path = args.trim();
if path.is_empty() {
ceprintln!(" [thuli::header]usage[/] /load <path>");
return;
}
let contents = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
ceprint!("[thuli::error][/]: could not read '");
eprintln!("{path}': {e}");
return;
}
};
let config = match parse(&contents) {
Ok(c) => c,
Err(e) => {
ceprintln!("[thuli::error][/]: parse error");
eprintln!("{e}");
return;
}
};
let mut styles_added = 0;
let mut prefixes_added = 0;
let mut skipped: Vec<String> = Vec::new();
for (name, markup) in &config.styles {
if name.starts_with("thuli::") {
skipped.push(name.clone());
continue;
}
match farben::Style::parse(markup) {
Ok(style) => {
{
let mut list = user_styles().lock().unwrap();
if !list.iter().any(|s| s == name) {
list.push(name.clone());
}
}
insert_style(name, style);
styles_added += 1;
}
Err(e) => {
ceprint!("[thuli::error][/]: style '");
eprint!("{name}': ");
eprintln!("{e}");
}
}
}
for (name, prefix) in &config.prefixes {
if name.starts_with("thuli::") {
skipped.push(format!("{name} (prefix)"));
continue;
}
if let Err(e) = set_prefix(name, prefix) {
ceprint!("[thuli::error][/]: prefix '");
eprint!("{name}': ");
eprintln!("{e}");
} else {
prefixes_added += 1;
}
}
println!(" \u{2192} loaded {styles_added} styles, {prefixes_added} prefixes from {path}");
if !skipped.is_empty() {
cprintln!(" [dim](skipped {} thuli::* names)[/]", skipped.len());
}
}