use colored::Colorize;
use serde::{Deserialize, Serialize};
const DEFAULT_BANNER: &str = include_str!("../../assets/banners/banner0.2.3.txt");
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct TerminalConfig {
pub banner: Option<String>,
pub prompt: String,
pub continuation_prompt: String,
pub color_enabled: bool,
pub edit_mode: EditMode,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum EditMode {
#[default]
Emacs,
Vi,
}
impl Default for TerminalConfig {
fn default() -> Self {
Self {
banner: Some(DEFAULT_BANNER.to_string()),
prompt: "oxur> ".to_string(),
continuation_prompt: "....> ".to_string(),
color_enabled: true,
edit_mode: EditMode::Emacs,
}
}
}
impl TerminalConfig {
pub fn builder() -> TerminalConfigBuilder {
TerminalConfigBuilder::new()
}
pub fn formatted_prompt(&self) -> String {
if self.color_enabled {
if self.prompt.starts_with("oxur") {
let rest = &self.prompt[4..]; format!(
"{}{}{}{}{}",
"o".truecolor(240, 120, 45),
"x".truecolor(195, 90, 30),
"u".truecolor(135, 60, 15),
"r".truecolor(105, 45, 15),
rest.truecolor(0, 255, 0),
)
} else {
self.prompt.green().to_string()
}
} else {
self.prompt.clone()
}
}
pub fn formatted_continuation_prompt(&self) -> String {
if self.color_enabled {
self.continuation_prompt.green().to_string()
} else {
self.continuation_prompt.clone()
}
}
pub fn merge(&mut self, other: TerminalConfig) {
if other.banner.is_some() {
self.banner = other.banner;
}
self.prompt = other.prompt;
self.continuation_prompt = other.continuation_prompt;
self.color_enabled = other.color_enabled;
self.edit_mode = other.edit_mode;
}
}
#[derive(Debug, Clone)]
pub struct TerminalConfigBuilder {
config: TerminalConfig,
}
impl TerminalConfigBuilder {
pub fn new() -> Self {
Self { config: TerminalConfig::default() }
}
pub fn banner(mut self, banner: impl Into<String>) -> Self {
self.config.banner = Some(banner.into());
self
}
pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
self.config.prompt = prompt.into();
self
}
pub fn continuation_prompt(mut self, prompt: impl Into<String>) -> Self {
self.config.continuation_prompt = prompt.into();
self
}
pub fn color(mut self, enabled: bool) -> Self {
self.config.color_enabled = enabled;
self
}
pub fn edit_mode(mut self, mode: EditMode) -> Self {
self.config.edit_mode = mode;
self
}
pub fn build(self) -> TerminalConfig {
self.config
}
}
impl Default for TerminalConfigBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = TerminalConfig::default();
assert_eq!(config.prompt, "oxur> ");
assert_eq!(config.continuation_prompt, "....> ");
assert!(config.color_enabled);
assert_eq!(config.edit_mode, EditMode::Emacs);
assert!(config.banner.is_some());
}
#[test]
fn test_builder() {
let config = TerminalConfig::builder()
.banner("Welcome!")
.prompt("λ> ")
.continuation_prompt(" | ")
.color(false)
.edit_mode(EditMode::Vi)
.build();
assert_eq!(config.banner, Some("Welcome!".to_string()));
assert_eq!(config.prompt, "λ> ");
assert_eq!(config.continuation_prompt, " | ");
assert!(!config.color_enabled);
assert_eq!(config.edit_mode, EditMode::Vi);
}
#[test]
#[serial_test::serial]
fn test_formatted_prompt_with_color() {
colored::control::set_override(true);
let config = TerminalConfig::builder().prompt("test> ").color(true).build();
let colored_prompt = config.formatted_prompt();
assert_ne!(colored_prompt, "test> ");
assert!(colored_prompt.contains("\x1b["));
assert!(colored_prompt.contains("test> "));
colored::control::unset_override();
}
#[test]
#[serial_test::serial]
fn test_formatted_prompt_oxur_colors() {
colored::control::set_override(true);
let config = TerminalConfig::builder().prompt("oxur> ").color(true).build();
let prompt = config.formatted_prompt();
assert_ne!(prompt, "oxur> ");
assert!(prompt.contains("\x1b["));
assert!(prompt.contains("o"));
assert!(prompt.contains("x"));
assert!(prompt.contains("u"));
assert!(prompt.contains("r"));
assert!(prompt.contains("> "));
colored::control::unset_override();
}
#[test]
fn test_formatted_prompt_without_color() {
let config = TerminalConfig::builder().prompt("test> ").color(false).build();
assert_eq!(config.formatted_prompt(), "test> ");
}
#[test]
fn test_serde_roundtrip() {
let config = TerminalConfig::builder()
.banner("Test Banner")
.prompt(">>> ")
.edit_mode(EditMode::Vi)
.build();
let toml = toml::to_string(&config).unwrap();
let parsed: TerminalConfig = toml::from_str(&toml).unwrap();
assert_eq!(config.banner, parsed.banner);
assert_eq!(config.prompt, parsed.prompt);
assert_eq!(config.edit_mode, parsed.edit_mode);
}
#[test]
fn test_edit_mode_serde() {
#[derive(Debug, serde::Deserialize)]
struct Wrapper {
mode: EditMode,
}
let emacs: Wrapper = toml::from_str("mode = \"emacs\"").unwrap();
let vi: Wrapper = toml::from_str("mode = \"vi\"").unwrap();
assert_eq!(emacs.mode, EditMode::Emacs);
assert_eq!(vi.mode, EditMode::Vi);
}
#[test]
fn test_default_banner_embedded() {
let config = TerminalConfig::default();
let banner = config.banner.expect("Default config should have banner");
assert!(banner.contains("Welcome to"));
assert!(banner.contains("oxur:"));
assert!(banner.contains("https://oxur.ελ/")); assert!(banner.contains("https://github.com/oxur/oxur/"));
assert!(banner.contains("(help)"));
assert!(banner.contains("(quit)"));
assert!(banner.len() > 1000);
assert!(banner.len() < 30000);
}
}