use std::cmp::max;
use std::io::{Result, Write};
use std::iter::zip;
use anstyle::Style;
use pulldown_cmark::{Alignment, BlockQuoteKind, CodeBlockKind, HeadingLevel};
use syntect::highlighting::HighlightState;
use syntect::parsing::{ParseState, ScopeStack};
use textwrap::core::{display_width, Word};
use textwrap::WordSeparator;
use crate::references::*;
use crate::render::data::{CurrentLine, CurrentTable, LinkReferenceDefinition, TableCell};
use crate::render::highlighting::highlighter;
use crate::render::state::*;
use crate::terminal::capabilities::{MarkCapability, StyleCapability, TerminalCapabilities};
use crate::terminal::osc::{clear_link, set_link_url};
use crate::terminal::TerminalSize;
use crate::Theme;
use crate::{Environment, Settings};
pub fn write_indent<W: Write>(writer: &mut W, level: u16) -> Result<()> {
write!(writer, "{}", " ".repeat(level as usize))
}
pub fn write_line_start<W: Write>(
writer: &mut W,
capabilities: &TerminalCapabilities,
theme: &Theme,
indent: u16,
quote_bar_cols: &[u16],
) -> Result<()> {
if quote_bar_cols.is_empty() {
return write_indent(writer, indent);
}
let style = Style::new().fg_color(Some(theme.quote_bar_color));
let mut col: u16 = 0;
for &bar_col in quote_bar_cols {
if bar_col >= col {
write_indent(writer, bar_col - col)?;
col = bar_col;
}
write_styled(writer, capabilities, &style, "\u{258C} ")?;
col += 2;
}
if col < indent {
write_indent(writer, indent - col)?;
}
Ok(())
}
pub fn write_alert_label<W: Write>(
writer: &mut W,
capabilities: &TerminalCapabilities,
theme: &Theme,
attrs: &StyledBlockAttrs,
kind: BlockQuoteKind,
) -> Result<()> {
write_line_start(
writer,
capabilities,
theme,
attrs.indent,
&attrs.quote_bar_cols,
)?;
let (label, color) = match kind {
BlockQuoteKind::Note => ("NOTE", anstyle::AnsiColor::Blue),
BlockQuoteKind::Tip => ("TIP", anstyle::AnsiColor::Green),
BlockQuoteKind::Important => ("IMPORTANT", anstyle::AnsiColor::Magenta),
BlockQuoteKind::Warning => ("WARNING", anstyle::AnsiColor::Yellow),
BlockQuoteKind::Caution => ("CAUTION", anstyle::AnsiColor::Red),
};
let style = Style::new().bold().fg_color(Some(color.into()));
write_styled(writer, capabilities, &style, label)?;
writeln!(writer)
}
pub fn write_styled<W: Write, S: AsRef<str>>(
writer: &mut W,
capabilities: &TerminalCapabilities,
style: &Style,
text: S,
) -> Result<()> {
match capabilities.style {
None => write!(writer, "{}", text.as_ref()),
Some(StyleCapability::Ansi) => write!(
writer,
"{}{}{}",
style.render(),
text.as_ref(),
style.render_reset()
),
}
}
#[allow(clippy::too_many_arguments)]
fn write_remaining_lines<W: Write>(
writer: &mut W,
capabilities: &TerminalCapabilities,
theme: &Theme,
style: &Style,
indent: u16,
quote_bar_cols: &[u16],
mut buffer: String,
next_lines: &[&[Word]],
last_line: &[Word],
) -> Result<CurrentLine> {
writeln!(writer)?;
write_line_start(writer, capabilities, theme, indent, quote_bar_cols)?;
for line in next_lines {
match line.split_last() {
None => {
}
Some((last, heads)) => {
for word in heads {
buffer.push_str(word.word);
buffer.push_str(word.whitespace);
}
buffer.push_str(last.word);
write_styled(writer, capabilities, style, &buffer)?;
writeln!(writer)?;
write_line_start(writer, capabilities, theme, indent, quote_bar_cols)?;
buffer.clear();
}
};
}
match last_line.split_last() {
None => {
Ok(CurrentLine::empty())
}
Some((last, heads)) => {
for word in heads {
buffer.push_str(word.word);
buffer.push_str(word.whitespace);
}
buffer.push_str(last.word);
write_styled(writer, capabilities, style, &buffer)?;
Ok(CurrentLine {
length: textwrap::core::display_width(&buffer) as u16,
trailing_space: Some(last.whitespace.to_owned()),
})
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn write_styled_and_wrapped<W: Write, S: AsRef<str>>(
writer: &mut W,
capabilities: &TerminalCapabilities,
theme: &Theme,
style: &Style,
max_width: u16,
indent: u16,
quote_bar_cols: &[u16],
current_line: CurrentLine,
text: S,
) -> Result<CurrentLine> {
let words = WordSeparator::UnicodeBreakProperties
.find_words(text.as_ref())
.collect::<Vec<_>>();
match words.first() {
None => Ok(current_line),
Some(first_word) => {
let current_width = current_line.length
+ indent
+ current_line
.trailing_space
.as_ref()
.map_or(0, |s| display_width(s.as_ref()) as u16);
if 0 < current_line.length
&& max_width < current_width + display_width(first_word) as u16
{
writeln!(writer)?;
write_line_start(writer, capabilities, theme, indent, quote_bar_cols)?;
return write_styled_and_wrapped(
writer,
capabilities,
theme,
style,
max_width,
indent,
quote_bar_cols,
CurrentLine::empty(),
text,
);
}
let widths = [
(max_width - current_width.min(max_width)) as f64,
(max_width - indent) as f64,
];
let lines = textwrap::wrap_algorithms::wrap_first_fit(&words, &widths);
match lines.split_first() {
None => {
Ok(current_line)
}
Some((first_line, tails)) => {
let mut buffer = String::with_capacity(max_width as usize);
let new_current_line = match first_line.split_last() {
None => {
current_line
}
Some((last, heads)) => {
if let Some(s) = current_line.trailing_space {
buffer.push_str(&s);
}
for word in heads {
buffer.push_str(word.word);
buffer.push_str(word.whitespace);
}
buffer.push_str(last.word);
let length =
current_line.length + textwrap::core::display_width(&buffer) as u16;
write_styled(writer, capabilities, style, &buffer)?;
buffer.clear();
CurrentLine {
length,
trailing_space: Some(last.whitespace.to_owned()),
}
}
};
match tails.split_last() {
None => {
Ok(new_current_line)
}
Some((last_line, next_lines)) => write_remaining_lines(
writer,
capabilities,
theme,
style,
indent,
quote_bar_cols,
buffer,
next_lines,
last_line,
),
}
}
}
}
}
}
pub fn write_mark<W: Write>(writer: &mut W, capabilities: &TerminalCapabilities) -> Result<()> {
if let Some(mark) = capabilities.marks {
match mark {
MarkCapability::ITerm2(marks) => marks.set_mark(writer),
}
} else {
Ok(())
}
}
pub fn write_rule<W: Write>(
writer: &mut W,
capabilities: &TerminalCapabilities,
theme: &Theme,
length: u16,
) -> std::io::Result<()> {
let rule = "\u{2550}".repeat(length as usize);
write_styled(
writer,
capabilities,
&Style::new().fg_color(Some(theme.rule_color)),
rule,
)
}
pub fn write_link_refs<W: Write>(
writer: &mut W,
environment: &Environment,
capabilities: &TerminalCapabilities,
links: Vec<LinkReferenceDefinition>,
) -> Result<()> {
if !links.is_empty() {
writeln!(writer)?;
for link in links {
write_styled(
writer,
capabilities,
&link.style,
format!("[{}]: ", link.index),
)?;
if let Some(url) = environment.resolve_reference(&link.target) {
match &capabilities.style {
Some(StyleCapability::Ansi) => {
set_link_url(writer, url, &environment.hostname)?;
write_styled(writer, capabilities, &link.style, link.target)?;
clear_link(writer)?;
}
None => write_styled(writer, capabilities, &link.style, link.target)?,
};
} else {
write_styled(writer, capabilities, &link.style, link.target)?;
}
if !link.title.is_empty() {
write_styled(
writer,
capabilities,
&link.style,
format!(" {}", link.title),
)?;
}
writeln!(writer)?;
}
};
Ok(())
}
pub fn code_block_inner_width(terminal_size: &TerminalSize, indent: u16) -> u16 {
terminal_size
.columns
.saturating_sub(indent)
.saturating_sub(4)
.max(1)
}
pub fn write_code_block_top<W: Write>(
writer: &mut W,
capabilities: &TerminalCapabilities,
theme: &Theme,
terminal_size: &TerminalSize,
indent: u16,
language: Option<&str>,
) -> std::io::Result<()> {
let style = Style::new().fg_color(Some(theme.code_block_border_color));
write_indent(writer, indent)?;
let inner = code_block_inner_width(terminal_size, indent) as usize;
let label = language.filter(|s| !s.is_empty()).unwrap_or("");
let label_piece = if label.is_empty() {
String::new()
} else {
format!(" {label} ")
};
let label_w = display_width(&label_piece);
let fill_count = (inner + 4).saturating_sub(4 + label_w);
let fill: String = std::iter::repeat_n('\u{2500}', fill_count).collect();
let line = format!("\u{256D}\u{2500}\u{2500}{label_piece}{fill}\u{256E}");
write_styled(writer, capabilities, &style, line)?;
writeln!(writer)
}
pub fn write_code_block_bottom<W: Write>(
writer: &mut W,
capabilities: &TerminalCapabilities,
theme: &Theme,
terminal_size: &TerminalSize,
indent: u16,
) -> std::io::Result<()> {
let style = Style::new().fg_color(Some(theme.code_block_border_color));
write_indent(writer, indent)?;
let inner = code_block_inner_width(terminal_size, indent) as usize;
let body: String = std::iter::repeat_n('\u{2500}', inner + 2).collect();
let line = format!("\u{2570}{body}\u{256F}");
write_styled(writer, capabilities, &style, line)?;
writeln!(writer)
}
pub fn write_code_line_suffix<W: Write>(
writer: &mut W,
capabilities: &TerminalCapabilities,
theme: &Theme,
terminal_size: &TerminalSize,
indent: u16,
content_width: u16,
) -> std::io::Result<()> {
let style = Style::new().fg_color(Some(theme.code_block_border_color));
let inner = code_block_inner_width(terminal_size, indent);
let padding = inner.saturating_sub(content_width) as usize;
if padding > 0 {
write!(writer, "{}", " ".repeat(padding))?;
}
write_styled(writer, capabilities, &style, " \u{2502}")?;
writeln!(writer)
}
pub fn write_start_code_block<W: Write>(
writer: &mut W,
settings: &Settings,
indent: u16,
style: Style,
block_kind: CodeBlockKind<'_>,
) -> Result<StackedState> {
let language: Option<&str> = match &block_kind {
CodeBlockKind::Fenced(name) if !name.is_empty() => Some(name.as_ref()),
_ => None,
};
write_code_block_top(
writer,
&settings.terminal_capabilities,
&settings.theme,
&settings.terminal_size,
indent,
language,
)?;
match (&settings.terminal_capabilities.style, block_kind) {
(Some(StyleCapability::Ansi), CodeBlockKind::Fenced(name)) if !name.is_empty() => {
match settings.syntax_set.find_syntax_by_token(&name) {
Some(syntax) => {
let parse_state = ParseState::new(syntax);
let highlight_state = HighlightState::new(highlighter(), ScopeStack::new());
Ok(HighlightBlockAttrs {
parse_state,
highlight_state,
indent,
}
.into())
}
None => Ok(LiteralBlockAttrs { indent, style }.into()),
}
}
(_, _) => Ok(LiteralBlockAttrs { indent, style }.into()),
}
}
#[allow(clippy::unnecessary_wraps)]
pub fn write_start_heading<W: Write>(
_writer: &mut W,
_capabilities: &TerminalCapabilities,
style: Style,
_level: HeadingLevel,
) -> Result<StackedState> {
Ok(StackedState::Inline(
InlineState::InlineBlock,
InlineAttrs {
style,
indent: 0,
quote_bar_cols: Vec::new(),
},
))
}
pub fn write_heading_rule<W: Write>(
writer: &mut W,
capabilities: &TerminalCapabilities,
style: Style,
level: HeadingLevel,
indent: u16,
terminal_size: &TerminalSize,
) -> std::io::Result<()> {
let glyph = match level {
HeadingLevel::H1 => '\u{2550}', HeadingLevel::H2 => '\u{2500}', _ => return Ok(()),
};
let length = terminal_size.columns.saturating_sub(indent);
if length == 0 {
return Ok(());
}
write_indent(writer, indent)?;
let rule: String = std::iter::repeat_n(glyph, length as usize).collect();
write_styled(writer, capabilities, &style, rule)?;
writeln!(writer)
}
pub(crate) fn display_width_with_emoji(s: &str) -> usize {
display_width(s) + s.chars().filter(|&c| c == '\u{FE0F}').count()
}
fn calculate_column_widths(table: &CurrentTable) -> Option<Vec<usize>> {
let first_row = table.head.as_ref().or(table.rows.first())?;
let mut widths = vec![0; first_row.cells.len()];
let rows = table.head.iter().chain(table.rows.as_slice());
for row in rows {
let current = row.cells.as_slice().iter().map(|cell| {
cell.fragments
.as_slice()
.iter()
.map(|s| display_width_with_emoji(s))
.sum::<usize>()
});
widths = zip(widths, current).map(|(a, b)| max(a, b)).collect();
}
Some(widths)
}
fn format_table_cell(cell: TableCell, width: usize, alignment: Alignment) -> String {
use Alignment::*;
let content = cell.fragments.join("");
let content = truncate_to_width(&content, width);
let displayed = display_width_with_emoji(&content);
let pad = width.saturating_sub(displayed);
let (left_pad, right_pad) = match alignment {
Left | None => (0, pad),
Right => (pad, 0),
Center => (pad / 2, pad - pad / 2),
};
let spaces = |n| " ".repeat(n);
format!(" {}{}{} ", spaces(left_pad), content, spaces(right_pad))
}
fn truncate_to_width(s: &str, max_width: usize) -> String {
if display_width_with_emoji(s) <= max_width {
return s.to_string();
}
if max_width == 0 {
return String::new();
}
let budget = max_width.saturating_sub(1);
let mut out = String::new();
let mut width = 0usize;
for ch in s.chars() {
let w = display_width(ch.encode_utf8(&mut [0; 4])) + usize::from(ch == '\u{FE0F}');
if width + w > budget {
break;
}
out.push(ch);
width += w;
}
out.push('\u{2026}');
out
}
fn write_box_rule<W: Write>(
writer: &mut W,
capabilities: &TerminalCapabilities,
theme: &Theme,
widths: &[usize],
left: char,
mid: char,
right: char,
) -> Result<()> {
let mut line = String::new();
line.push(left);
for (i, &width) in widths.iter().enumerate() {
line.extend(std::iter::repeat_n('\u{2500}', width + 2));
line.push(if i + 1 == widths.len() { right } else { mid });
}
write_styled(
writer,
capabilities,
&Style::new().fg_color(Some(theme.rule_color)),
line,
)?;
writeln!(writer)
}
fn write_box_sep<W: Write>(
writer: &mut W,
capabilities: &TerminalCapabilities,
theme: &Theme,
) -> Result<()> {
write_styled(
writer,
capabilities,
&Style::new().fg_color(Some(theme.rule_color)),
"\u{2502}",
)
}
pub fn write_table<W: Write>(
writer: &mut W,
capabilities: &TerminalCapabilities,
theme: &Theme,
terminal_size: &TerminalSize,
table: CurrentTable,
) -> Result<()> {
let Some(widths) = calculate_column_widths(&table) else {
return Ok(());
};
let chrome = 1 + widths.len() as u16 + 2 * widths.len() as u16;
let max_inner = terminal_size.columns.saturating_sub(chrome);
let widths = shrink_widths_to_fit(widths, max_inner as usize);
write_box_rule(
writer,
capabilities,
theme,
&widths,
'\u{250C}',
'\u{252C}',
'\u{2510}',
)?;
if let Some(head) = table.head {
for ((cell, &width), &alignment) in zip(zip(head.cells, &widths), &table.alignments) {
write_box_sep(writer, capabilities, theme)?;
write_styled(
writer,
capabilities,
&Style::new().bold(),
format_table_cell(cell, width, alignment),
)?;
}
write_box_sep(writer, capabilities, theme)?;
writeln!(writer)?;
write_box_rule(
writer,
capabilities,
theme,
&widths,
'\u{251C}',
'\u{253C}',
'\u{2524}',
)?;
}
for row in table.rows {
for ((cell, &width), &alignment) in zip(zip(row.cells, &widths), &table.alignments) {
write_box_sep(writer, capabilities, theme)?;
write_styled(
writer,
capabilities,
&Style::new(),
format_table_cell(cell, width, alignment),
)?;
}
write_box_sep(writer, capabilities, theme)?;
writeln!(writer)?;
}
write_box_rule(
writer,
capabilities,
theme,
&widths,
'\u{2514}',
'\u{2534}',
'\u{2518}',
)
}
fn shrink_widths_to_fit(mut widths: Vec<usize>, max_inner: usize) -> Vec<usize> {
loop {
let total: usize = widths.iter().sum();
if total <= max_inner || widths.iter().all(|&w| w == 0) {
return widths;
}
if let Some((i, _)) = widths.iter().enumerate().max_by_key(|(_, &w)| w) {
if widths[i] > 0 {
widths[i] -= 1;
} else {
return widths;
}
}
}
}