use std::{
cmp::max,
iter::{self},
};
use directories::ProjectDirs;
use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Paragraph, StatefulWidget, Widget},
};
use syntect::{
easy::HighlightLines,
highlighting::{FontStyle, Theme, ThemeSet},
parsing::SyntaxSet,
};
use unicode_width::UnicodeWidthStr;
use crate::core::conversation::{
Block, ConversationItem, ConversationItemKind, Document, Inline, ListItem,
ListItemMarker::{self, Bullet},
NotificationType, TableRow, ToolInvocationContent, ToolOutputContent,
};
type SyntectStyle = syntect::highlighting::Style;
const PAGE_SIZE: u16 = 40;
const GUTTER_WIDTH: u16 = 2;
const LIGHT_BLUE: Color = Color::Rgb(0, 122, 204);
const PURPLE: Color = Color::Rgb(197, 134, 192);
const LIGHT_GRAY: Color = Color::Rgb(205, 205, 205);
const DARK_GRAY2: Color = Color::Rgb(139, 148, 158);
const DARK_GRAY3: Color = Color::Rgb(40, 40, 40);
const NEAR_BLACK: Color = Color::Rgb(20, 20, 20);
const NEAR_BLACK2: Color = Color::Rgb(30, 30, 30);
const METALLIC_BLUE: Color = Color::Rgb(101, 154, 211);
const LIGHT_TOURQUOISE: Color = Color::Rgb(168, 218, 216);
const TOURQUOISE: Color = Color::Rgb(82, 197, 176);
const DARK_PURPLE: Color = Color::Rgb(97, 40, 119);
const DARK_BLUE: Color = Color::Rgb(20, 56, 92);
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum ScrollingPosition {
#[default]
Auto,
Position(u16), }
#[derive(Clone, Debug)]
struct TextStyle {
fg_color: Color,
bg_color: Color,
bold: bool,
italic: bool,
underline: bool,
strikethrough: bool,
}
impl Default for TextStyle {
fn default() -> Self {
Self {
fg_color: LIGHT_GRAY,
bg_color: NEAR_BLACK2,
bold: false,
italic: false,
underline: false,
strikethrough: false,
}
}
}
impl From<TextStyle> for Style {
fn from(val: TextStyle) -> Self {
let mut style = Style::new().fg(val.fg_color).bg(val.bg_color);
style = if val.bold { style.bold() } else { style };
style = if val.italic { style.italic() } else { style };
style = if val.underline {
style.underlined()
} else {
style
};
style = if val.strikethrough {
style.crossed_out()
} else {
style
};
style
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct RenderedTableCell {
content: Vec<Line<'static>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct RenderedTableRow {
content: Vec<RenderedTableCell>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct RenderedTable {
content: Vec<RenderedTableRow>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum InlineFragment {
Text(Span<'static>),
LineBreak,
}
#[derive(Clone, Debug, Default)]
pub struct HighlightConfig {
syntax_set: SyntaxSet,
theme: Theme,
}
impl HighlightConfig {
pub fn new() -> Self {
let syn_set = SyntaxSet::load_defaults_newlines();
let mut theme_set = ThemeSet::load_defaults();
if let Some(proj_dirs) = ProjectDirs::from("org", "codemonk", "monothink") {
let mut share_dir = proj_dirs.data_dir().to_path_buf();
share_dir.push("themes");
let share_path = share_dir.as_path();
theme_set
.add_from_folder(share_path)
.expect("Failed to load theme folder.");
}
let theme = theme_set.themes.get("darkplus").unwrap_or_else(|| {
theme_set
.themes
.get("base16-ocean.dark")
.expect("Cannot load default theme")
});
Self {
syntax_set: syn_set,
theme: theme.clone(),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct MessageProjection {
pub start: usize,
pub end: usize,
}
#[derive(Clone, Debug, Default)]
pub struct ConversationViewModel {
conversation_view: Vec<Line<'static>>,
pub conversation_items: Vec<ConversationItem>,
dirty: bool,
height: Option<u16>,
scrolling_position: ScrollingPosition,
highlight_cfg: HighlightConfig,
}
#[derive(Debug, Clone, Default)]
pub struct ConversationView;
impl StatefulWidget for ConversationView {
type State = ConversationViewModel;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
state.height = Some(area.height);
if state.dirty {
state.conversation_view.clear();
for item in &state.conversation_items {
state.conversation_view.extend(Self::render_item(
item,
area.width,
&state.highlight_cfg,
));
}
state.dirty = false;
}
let pos = if let Some(height) = state.height
&& state.scrolling_position == ScrollingPosition::Auto
{
if height < state.conversation_view.len() as u16 {
state.conversation_view.len() as u16 - height
} else {
0
}
} else if let ScrollingPosition::Position(fixed_pos) = state.scrolling_position {
fixed_pos
} else {
0
};
Paragraph::new(state.conversation_view.clone())
.scroll((pos, 0))
.style(TextStyle::default())
.render(area, buf);
}
}
impl ConversationViewModel {
pub fn new(highlight_cfg: HighlightConfig) -> Self {
Self {
highlight_cfg,
..Default::default()
}
}
pub fn append_item(&mut self, item: ConversationItem) {
self.dirty = true;
self.conversation_items.push(item);
}
pub fn update_last_item(&mut self, item: ConversationItem) {
if self.conversation_items.last().is_some() {
self.conversation_items.pop();
self.append_item(item);
}
}
pub fn move_line_up(&mut self) {
self.move_up(1);
}
pub fn move_line_down(&mut self) {
self.move_down(1)
}
pub fn move_page_up(&mut self) {
self.move_up(PAGE_SIZE);
}
pub fn move_page_down(&mut self) {
self.move_down(PAGE_SIZE)
}
fn move_down(&mut self, step: u16) {
if !self.can_scroll() {
return;
}
if let Some(height) = self.height {
let p = self.calculate_position();
if p + height < self.conversation_view.len() as u16 - step {
self.scrolling_position = ScrollingPosition::Position(p + step);
} else {
self.scrolling_position = ScrollingPosition::Auto;
}
}
}
fn move_up(&mut self, step: u16) {
let p = self.calculate_position();
if p >= step {
self.scrolling_position = ScrollingPosition::Position(p - step);
} else {
self.scrolling_position = ScrollingPosition::Position(0);
}
}
fn calculate_position(&self) -> u16 {
if !self.can_scroll() {
0
} else {
match self.scrolling_position {
ScrollingPosition::Auto => {
let h = self.height.expect("Height must be set.");
self.conversation_view.len() as u16 - h
}
ScrollingPosition::Position(p) => p,
}
}
}
fn can_scroll(&self) -> bool {
if let Some(height) = self.height {
height < self.conversation_view.len() as u16
} else {
false
}
}
}
impl ConversationView {
pub fn render_item(
item: &ConversationItem,
width: u16,
highlight_cfg: &HighlightConfig,
) -> Vec<Line<'static>> {
let mut lines = match &item.content {
ConversationItemKind::UserMessage(msg) => {
Self::render_user(msg.content(), width, highlight_cfg)
}
ConversationItemKind::AssistantMessage(msg) => {
Self::render_assistant(msg.content(), width, highlight_cfg)
}
ConversationItemKind::AssistantReasoning(msg) => {
Self::render_thinking(msg.content(), width, highlight_cfg)
}
ConversationItemKind::Notification(notification_type, content) => {
Self::render_notification(notification_type.to_owned(), content, width)
}
ConversationItemKind::ToolInvocation(ToolInvocationContent { name, arguments }) => {
Self::render_tool_invocation(name, arguments)
}
ConversationItemKind::ToolOutput(ToolOutputContent { output, success }) => {
Self::render_tool_result(output, *success)
}
};
lines.push(Line::raw(""));
lines
}
fn render_assistant(
msg: &Document,
width: u16,
highlight_cfg: &HighlightConfig,
) -> Vec<Line<'static>> {
Self::render_message(
|w| {
let mut lines = Vec::new();
for block in &msg.blocks {
lines.extend(Self::render_block(
block,
w,
TextStyle::default(),
highlight_cfg,
));
}
lines
},
LIGHT_BLUE,
DARK_BLUE,
width,
"🤖 Assistant",
)
}
fn render_thinking(
msg: &Document,
width: u16,
highlight_cfg: &HighlightConfig,
) -> Vec<Line<'static>> {
Self::render_message(
|w| {
let mut lines = Vec::new();
for block in &msg.blocks {
lines.extend(Self::render_block(
block,
w,
TextStyle::default(),
highlight_cfg,
));
}
lines
},
LIGHT_TOURQUOISE,
TOURQUOISE,
width,
"Thinking",
)
}
fn render_user(
msg: &Document,
width: u16,
highlight_cfg: &HighlightConfig,
) -> Vec<Line<'static>> {
Self::render_message(
|w| {
let mut lines = Vec::new();
for block in &msg.blocks {
lines.extend(Self::render_block(
block,
w,
TextStyle::default(),
highlight_cfg,
));
}
lines
},
PURPLE,
DARK_PURPLE,
width,
"🧑💻 You",
)
}
fn render_block(
block: &Block,
width: u16,
style: TextStyle,
highlight_cfg: &HighlightConfig,
) -> Vec<Line<'static>> {
let mut lines = Vec::new();
match block {
Block::Code {
lang,
content,
copy_id,
} => lines.extend(Self::render_code(
content,
lang,
*copy_id,
width,
style.clone(),
highlight_cfg,
)),
Block::Heading { depth, content } => {
lines.extend(Self::render_heading(content, *depth, width, style.clone()))
}
Block::List { content } => lines.extend(Self::render_list(
content,
width,
style.clone(),
highlight_cfg,
)),
Block::Paragraph(inlines) => {
lines.extend(Self::render_paragraph(width, inlines, style.clone()))
}
Block::Quote(blocks) => lines.extend(Self::render_quote(
blocks,
width,
style.clone(),
highlight_cfg,
)),
Block::Table { content } => {
lines.extend(Self::render_table(content, width, style.clone()))
}
Block::ThematicBreak => {
let delim_style = TextStyle {
fg_color: DARK_GRAY2,
..style
};
lines.push(Line::styled("────────────────", delim_style))
}
Block::Unsupported(s) => {
let warning_style = TextStyle {
fg_color: Color::Red,
..style
};
lines.push(Line::from(Span::styled(
format!("Warning: Unsupported block in markdown: {s}"),
warning_style,
)))
}
}
lines.push(Line::raw(" "));
lines
}
fn render_inline(inline: &Inline, style: TextStyle) -> Vec<InlineFragment> {
let mut fragments = Vec::new();
match inline {
Inline::Code(code) => {
let mut s = style.clone();
s.bg_color = NEAR_BLACK;
fragments.push(InlineFragment::Text(Span::styled(code.to_owned(), s)))
}
Inline::Emphasis(inlines) => fragments.extend(
inlines
.iter()
.flat_map(|inline| {
let mut s = style.clone();
s.italic = true;
Self::render_inline(inline, s)
})
.collect::<Vec<InlineFragment>>(),
),
Inline::StrongEmphasis(inlines) => fragments.extend(
inlines
.iter()
.flat_map(|inline| {
let mut s = style.clone();
s.bold = true;
Self::render_inline(inline, s)
})
.collect::<Vec<InlineFragment>>(),
),
Inline::Link { url, title, label } => fragments.push(InlineFragment::Text(
Span::styled(url.to_owned(), Style::from(style)),
)),
Inline::Image(_) => {}
Inline::LineBreak => fragments.push(InlineFragment::LineBreak),
Inline::Text(text) => fragments.push(InlineFragment::Text(Span::styled(
text.to_owned(),
Style::from(style),
))),
Inline::Unsupported(_) => {
}
}
fragments
}
fn render_message<T: FnOnce(u16) -> Vec<Line<'static>>>(
body: T,
text_color: Color,
gutter_color: Color,
width: u16,
header: &str,
) -> Vec<Line<'static>> {
let mut lines = vec![Self::render_header(header, text_color), Line::raw(" ")];
lines.extend(body(width - GUTTER_WIDTH));
Self::add_gutter(lines, gutter_color, None)
}
fn add_gutter(
lines: Vec<Line<'static>>,
color: Color,
bg_color: Option<Color>,
) -> Vec<Line<'static>> {
lines
.into_iter()
.map(|line| {
let mut style = Style::default().fg(color);
style.bg = bg_color;
let mut spans = Vec::with_capacity(line.spans.len() + 1);
spans.push(Span::styled("▌ ", style));
spans.extend(line.spans);
Line::from(spans)
})
.collect()
}
fn render_header(label: &str, color: Color) -> Line<'static> {
Line::from(vec![Span::styled(
label.to_string(),
Style::default().fg(color).add_modifier(Modifier::BOLD),
)])
}
fn render_tool_invocation(name: &str, args: &str) -> Vec<Line<'static>> {
let lines = vec![
Self::render_header(&format!("🔧 Tool: {}", name), Color::Rgb(220, 220, 170)),
Line::raw(" "),
Line::from(Span::styled(
format!("> {}", args),
Style::default().fg(Color::DarkGray),
)),
];
Self::add_gutter(lines, Color::Yellow, None)
}
fn render_tool_result(output: &str, success: bool) -> Vec<Line<'static>> {
let mut lines = vec![
Self::render_header("🔧 Tool Output", Color::Rgb(220, 220, 170)),
Line::raw(" "),
];
for line in output.lines() {
lines.push(Line::from(Span::raw(line.to_string())));
}
Self::add_gutter(lines, Color::Yellow, None)
}
fn render_notification(
notification_type: NotificationType,
msg: &str,
width: u16,
) -> Vec<Line<'static>> {
let lines = vec![
Self::render_header("⚙️ System", Color::Green),
Line::raw(" "),
Line::from(Span::raw(msg.to_string())),
];
Self::add_gutter(lines, Color::Rgb(62, 143, 126), None)
}
fn wrap_lines(lines: Vec<Line<'static>>, width: u16) -> Vec<Line<'static>> {
lines
.into_iter()
.flat_map(|line| Self::wrap_line(line, width, 0))
.collect()
}
fn wrap_line(line: Line<'static>, width: u16, position: usize) -> Vec<Line<'static>> {
let mut lines = Vec::new();
let mut current_line = Vec::new();
let mut position = position;
current_line.clear();
for span in line.spans {
let mut current_span = Some(span);
while let Some(s) = current_span {
match Self::wrap_span(s, width, position) {
(Some(span), None) => {
let increment = span.content.len();
current_line.push(span);
position += increment;
current_span = None; }
(Some(span1), Some(span2)) => {
current_line.push(span1);
lines.push(Line::from(current_line.clone()));
current_line.clear();
position = 0;
current_span = Some(span2);
}
(None, Some(span)) => {
lines.push(Line::from(current_line.clone()));
current_line.clear();
position = 0;
current_span = Some(span);
}
(None, None) => {
current_span = None;
}
}
}
}
if !current_line.is_empty() {
lines.push(Line::from(current_line));
}
lines
}
fn wrap_span(
span: Span<'static>,
width: u16,
position: usize,
) -> (Option<Span<'static>>, Option<Span<'static>>) {
let width: usize = width.into();
let style = span.style;
let mut pos = position;
let mut first = Vec::new();
let mut second = Vec::new();
for chunk in span.content.split_inclusive(char::is_whitespace) {
let w = UnicodeWidthStr::width(chunk);
pos += w;
if pos <= width {
first.push(chunk);
} else {
second.push(chunk);
}
}
if let Some(w) = first.last()
&& UnicodeWidthStr::width(*w) >= width
{
let (w1, w2) = w.split_at(width - pos);
first.pop();
first.push(w1);
second.insert(0, w2);
}
let res1 = if first.is_empty() {
None
} else {
Some(Span::from(first.join("")).style(style))
};
let res2 = if second.is_empty() {
None
} else {
Some(Span::from(second.join("")).style(style))
};
(res1, res2)
}
fn render_heading(
content: &[Inline],
depth: u8,
width: u16,
style: TextStyle,
) -> Vec<Line<'static>> {
let mut lines = Vec::new();
let marker = match depth {
0 => "█ ",
1 => "▊ ",
2 => "▌ ",
_ => "▎ ",
};
let heading_style = TextStyle {
bold: true,
fg_color: METALLIC_BLUE,
..style
};
let mut fragments = Vec::new();
for inline in content {
fragments.extend(Self::render_inline(inline, heading_style.clone()));
}
let mut content_lines = Self::wrap_lines(Self::fragments_to_lines(&fragments), width);
if let Some(first) = content_lines.get_mut(0) {
first
.spans
.insert(0, Span::styled(marker, heading_style.clone()));
}
for line in content_lines.iter_mut().skip(1) {
line.spans.insert(
0,
Span::styled(" ".repeat(marker.width()), heading_style.clone()),
);
}
if let Some(last) = content_lines.last_mut() {
let delim_style = TextStyle {
fg_color: DARK_GRAY2,
..style
};
last.push_span(Span::styled(
" ".to_owned() + &"─".repeat(7),
delim_style,
));
}
lines.extend(content_lines);
lines
}
fn render_list(
content: &[ListItem],
width: u16,
style: TextStyle,
highlight_cfg: &HighlightConfig,
) -> Vec<Line<'static>> {
let marker_indend = content.iter().fold(2, |current, item| {
let candidate = match item.marker {
ListItemMarker::Bullet => 2, ListItemMarker::Checkbox(_) => 2, ListItemMarker::Number(index) => index.to_string().len() + 2,
};
max(current, candidate)
});
content
.iter()
.flat_map(|item| {
Self::render_list_item(item, width, style.clone(), highlight_cfg, marker_indend)
})
.collect()
}
fn render_list_item(
item: &ListItem,
width: u16,
style: TextStyle,
highlight_cfg: &HighlightConfig,
marker_indend: usize,
) -> Vec<Line<'static>> {
let mut lines: Vec<Line<'static>> = item
.content
.iter()
.flat_map(|block| {
Self::render_block(
block,
width - marker_indend as u16,
style.clone(),
highlight_cfg,
)
})
.collect();
if let Some(first_line) = lines.first_mut() {
let marker = match item.marker {
Bullet => {
let marker = "• ".to_string();
Span::styled(
format!(
"{}{}",
marker,
" ".repeat(marker_indend.saturating_sub(marker.width()))
),
style.clone(),
)
}
ListItemMarker::Checkbox(checked) => {
let marker = (if checked { "☑ " } else { "☐ " }).to_string();
Span::styled(
format!(
"{}{}",
marker,
" ".repeat(marker_indend.saturating_sub(marker.width()))
),
style.clone(),
)
}
ListItemMarker::Number(n) => Span::styled(
format!(
"{}. {}",
n,
" ".repeat(marker_indend.saturating_sub(n as usize + 1))
),
style.clone(),
),
};
first_line.spans.insert(0, marker);
}
for line in &mut lines.iter_mut().skip(1) {
line.spans
.insert(0, Span::styled(" ".repeat(marker_indend), style.clone()));
}
lines
}
fn render_paragraph(width: u16, inlines: &[Inline], style: TextStyle) -> Vec<Line<'static>> {
let mut lines = Self::fragments_to_lines(
&inlines
.iter()
.flat_map(|inline| Self::render_inline(inline, style.clone()))
.collect::<Vec<InlineFragment>>(),
);
lines = Self::wrap_lines(lines, width);
lines
}
fn render_quote(
content: &[Block],
width: u16,
style: TextStyle,
highlight_cfg: &HighlightConfig,
) -> Vec<Line<'static>> {
let lines = content
.iter()
.flat_map(|block| {
Self::render_block(block, width - GUTTER_WIDTH, style.clone(), highlight_cfg)
})
.collect();
Self::add_gutter(lines, DARK_GRAY2, None)
}
fn render_table(content: &[TableRow], width: u16, style: TextStyle) -> Vec<Line<'static>> {
let separator_prefill = 1;
let separator_postfill = 1;
let separator = format!(
"{}│{}",
" ".repeat(separator_prefill),
" ".repeat(separator_postfill)
);
let indend = " ";
let num_columns = content
.iter()
.fold(0, |count, row| max(count, row.content.len()));
let rows: Vec<_> = content
.iter()
.map(|row| {
let mut cells: Vec<_> = row
.content
.iter()
.map(|cell| {
let fragments = cell
.content
.iter()
.flat_map(|inline| Self::render_inline(inline, style.clone()))
.collect::<Vec<_>>();
RenderedTableCell {
content: Self::fragments_to_lines(&fragments),
}
})
.collect();
while cells.len() < num_columns {
cells.push(RenderedTableCell {
content: vec![Line::raw("")],
});
}
RenderedTableRow { content: cells }
})
.collect();
let mut prerendered_table = RenderedTable { content: rows };
let mut max_column_widths = vec![0; num_columns];
for row in prerendered_table.content.iter() {
for (idx, cell) in row.content.iter().enumerate() {
let content_width = cell.content.iter().fold(0, |w, s| max(w, s.width()));
max_column_widths[idx] = max(content_width, max_column_widths[idx]);
}
}
let mut max_row_height = vec![0; prerendered_table.content.len()];
for (idx, row) in prerendered_table.content.iter().enumerate() {
for cell in row.content.iter() {
let content_height = cell.content.len();
if content_height > max_row_height[idx] {
max_row_height[idx] = content_height;
}
}
}
for (idx, row) in prerendered_table.content.iter_mut().enumerate() {
for cell in row.content.iter_mut() {
while cell.content.len() < max_row_height[idx] {
cell.content.push(Line::raw(""));
}
}
}
let mut lines = Vec::new();
if let Some(header) = prerendered_table.content.first() {
lines.extend(Self::render_table_row(
&header.content,
&max_column_widths,
indend,
&separator,
style.clone(),
));
}
let mut spans = Vec::new();
if let Some(column) = max_column_widths.first() {
spans.push(Span::styled(
format!("{}{}", indend, "─".repeat(*column)),
style.clone(),
));
}
spans.extend(max_column_widths.iter().skip(1).map(|l| {
Span::styled(
format!(
"{}┼{}",
"─".repeat(separator_prefill),
"─".repeat(l + separator_postfill)
),
style.clone(),
)
}));
lines.push(Line::from(spans));
lines.extend(prerendered_table.content.iter().skip(1).flat_map(|row| {
Self::render_table_row(
&row.content,
&max_column_widths,
indend,
&separator,
style.clone(),
)
}));
lines
}
fn render_table_row(
cells: &[RenderedTableCell],
column_widths: &[usize],
prefix: &str,
infix: &str,
style: TextStyle,
) -> Vec<Line<'static>> {
let mut sep = prefix;
if let Some(cell) = cells.first() {
let len = cell.content.len();
(0..len)
.map(|i| {
Line::from(
cells
.iter()
.enumerate()
.flat_map(|(j, cell)| {
let mut line = vec![Span::styled(sep.to_string(), style.clone())];
sep = infix;
let content_width =
cell.content[i].spans.iter().fold(0, |l, s| l + s.width());
let num_fill_chars = column_widths
.get(j)
.unwrap_or(&0)
.saturating_sub(content_width);
line.extend(cell.content[i].spans.clone());
line.push(Span::raw(" ".repeat(num_fill_chars)));
line
})
.collect::<Vec<_>>(),
)
})
.collect()
} else {
Vec::new()
}
}
fn render_code(
content: &str,
lang: &str,
copy_id: u32,
width: u16,
style: TextStyle,
highlight_cfg: &HighlightConfig,
) -> Vec<Line<'static>> {
let mut code_style = style.clone();
code_style.bg_color = DARK_GRAY3;
let mut header_style = style.clone();
header_style.bg_color = DARK_GRAY3;
header_style.italic = true;
let mut content_lines =
Self::highlight_code(content, lang, code_style.clone(), highlight_cfg); let mut max_width = content_lines
.iter()
.fold(0, |max_width, line| std::cmp::max(max_width, line.width()));
max_width += 1; max_width = std::cmp::min(max_width, width as usize);
for line in content_lines.iter_mut() {
line.push_span(Span::styled(
" ".repeat(max_width.saturating_sub(line.width())),
code_style.clone(),
))
}
let lang_marker = format!(" {}", lang);
let copy_marker = format!(" {} ", copy_id);
let header = iter::once(Line::from(Span::styled(
format!(
"{}{}{}",
lang_marker,
" ".repeat(max_width.saturating_sub(lang_marker.width() + copy_marker.width())),
copy_marker
),
Style::from(header_style),
)));
let lines = header.chain(content_lines).collect();
Self::add_gutter(lines, DARK_GRAY2, Some(DARK_GRAY3))
}
fn format_code(code: &str, lang: &str, style: TextStyle) -> Vec<Line<'static>> {
code.lines()
.map(|line| Line::from(Span::styled(line.to_string(), style.clone())))
.collect()
}
fn highlight_code(
code: &str,
lang: &str,
style: TextStyle,
highlight_cfg: &HighlightConfig,
) -> Vec<Line<'static>> {
let syntax = highlight_cfg
.syntax_set
.find_syntax_by_token(lang)
.unwrap_or_else(|| highlight_cfg.syntax_set.find_syntax_plain_text());
let mut highlighter = HighlightLines::new(syntax, &highlight_cfg.theme);
code.lines()
.map(|line| {
let ranges = highlighter
.highlight_line(line, &highlight_cfg.syntax_set)
.unwrap();
Line::from(Self::ranges_to_spans(ranges, style.clone()))
})
.collect()
}
fn ranges_to_spans(
ranges: Vec<(SyntectStyle, &str)>,
base_style: TextStyle,
) -> Vec<Span<'static>> {
ranges
.into_iter()
.map(|(style, text)| {
Span::styled(
text.to_string(),
Self::merge_syntect_style(style, base_style.clone()),
)
})
.collect()
}
fn merge_syntect_style(style: SyntectStyle, base_style: TextStyle) -> Style {
let mut result = Style::default()
.fg(Color::Rgb(
style.foreground.r,
style.foreground.g,
style.foreground.b,
))
.bg(base_style.bg_color);
if style.font_style.contains(FontStyle::BOLD) {
result = result.add_modifier(Modifier::BOLD);
}
if style.font_style.contains(FontStyle::ITALIC) {
result = result.add_modifier(Modifier::ITALIC);
}
if style.font_style.contains(FontStyle::UNDERLINE) {
result = result.add_modifier(Modifier::UNDERLINED);
}
result
}
fn fragments_to_lines(fragments: &[InlineFragment]) -> Vec<Line<'static>> {
fragments
.split(|fragment| *fragment == InlineFragment::LineBreak)
.map(|slice| {
slice
.iter()
.filter_map(|f| match f {
InlineFragment::Text(span) => Some(span.to_owned()),
InlineFragment::LineBreak => None, })
.collect::<Vec<Span<'static>>>()
})
.map(Line::from)
.collect()
}
}