use crate::{render_context::RenderContext, styled_string::Document};
use std::{
fmt::Write,
io::{self, IsTerminal},
};
mod ai;
mod interactive;
mod plain;
mod test_mode;
mod tty;
pub use interactive::{HistoryEntry, render_interactive};
const LIST_BULLETS: &[char] = &['◦', '▪', '•', '‣', '⁃'];
pub(crate) fn bullet_for_indent(indent: u16) -> char {
let nesting_level = (indent / 4) as usize;
LIST_BULLETS[nesting_level % LIST_BULLETS.len()]
}
#[cfg(test)]
pub use interactive::render_to_test_backend;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputMode {
Tty,
Plain,
TestMode,
Ai,
}
impl OutputMode {
pub fn detect() -> Self {
if std::env::var("FERRITIN_TEST_MODE").is_ok() {
OutputMode::TestMode
} else if std::env::var("CLAUDECODE").is_ok()
|| std::env::var("GEMINI_CLI").is_ok()
|| std::env::var("CODEX_SANDBOX").is_ok()
{
OutputMode::Ai
} else if io::stdout().is_terminal() {
OutputMode::Tty
} else {
OutputMode::Plain
}
}
}
pub fn render(
document: &Document,
render_context: &RenderContext,
output: &mut impl Write,
) -> std::fmt::Result {
match render_context.output_mode() {
OutputMode::Tty => tty::render(document, render_context, output),
OutputMode::Plain => plain::render(document, output),
OutputMode::TestMode => test_mode::render(document, output),
OutputMode::Ai => ai::render(document, output),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::styled_string::{DocumentNode, HeadingLevel, Span};
#[test]
fn test_render_modes() {
let doc = Document::with_nodes(vec![
DocumentNode::heading(
HeadingLevel::Title,
vec![Span::plain("Test"), Span::keyword("struct")],
),
DocumentNode::paragraph(vec![Span::type_name("Foo")]),
]);
let mut tty_output = String::new();
let mut plain_output = String::new();
let mut test_output = String::new();
render(
&doc,
&RenderContext::new().with_output_mode(OutputMode::Tty),
&mut tty_output,
)
.unwrap();
render(
&doc,
&RenderContext::new().with_output_mode(OutputMode::Plain),
&mut plain_output,
)
.unwrap();
render(
&doc,
&RenderContext::new().with_output_mode(OutputMode::TestMode),
&mut test_output,
)
.unwrap();
assert!(!tty_output.is_empty());
assert!(!plain_output.is_empty());
assert!(!test_output.is_empty());
}
}