use std::io::stdout;
use crossterm::event::{self, Event, KeyCode};
#[cfg(feature = "image-protocol")]
use ratatui::layout::Alignment as RatatuiAlignment;
#[cfg(feature = "image-protocol")]
use ratatui::layout::Size;
use ratatui::layout::Rect;
use ratatui::style::{Color, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
use ratatui::Terminal;
use limner::{render_markdown, Alignment, MarkdownStyle};
struct Section {
label: &'static str,
text: &'static str,
style: MarkdownStyle,
}
fn run(
sections: &[Section],
width: u16,
) -> (
Vec<Line<'static>>,
Vec<limner::ImageInfo>,
Vec<limner::LinkInfo>,
) {
let mut out = Vec::new();
let mut images = Vec::new();
let mut links = Vec::new();
for (i, s) in sections.iter().enumerate() {
if i > 0 {
let sep = format!("── {} ", s.label);
out.push(Line::from(Span::styled(
sep,
Style::new().fg(Color::Rgb(100, 100, 100)),
)));
}
let result = render_markdown(s.text, &s.style, width);
let offset = out.len();
out.extend(result.lines);
for img in result.images {
images.push(limner::ImageInfo {
line_index: img.line_index + offset,
..img
});
}
for link in result.links {
links.push(limner::LinkInfo {
line_index: link.line_index + offset,
..link
});
}
}
(out, images, links)
}
fn sections(
width: u16,
) -> (
Vec<Line<'static>>,
Vec<limner::ImageInfo>,
Vec<limner::LinkInfo>,
) {
let base = MarkdownStyle::default();
fn txt(s: &str) -> String {
s.to_string()
}
let sections = vec and an \
inline image: . \
Both should appear inline with proper alignment. The placeholder image \
text is part of the justified flow.",
)
.leak(),
style: MarkdownStyle {
paragraph_alignment: Alignment::Justify,
..base.clone()
},
},
Section {
label: "Image — Center",
text: txt(
"# Centered Image\n\n\
This paragraph is centered and contains an inline image: \
. \
The image should also be centered horizontally."
)
.leak(),
style: MarkdownStyle {
heading_1_alignment: Alignment::Center,
paragraph_alignment: Alignment::Center,
..base.clone()
},
},
Section {
label: "Image — Right",
text: txt(
"## Right-Aligned Image\n\n\
This paragraph is right-aligned with an image: \
. \
The image should hug the right edge of the terminal."
)
.leak(),
style: MarkdownStyle {
heading_2_alignment: Alignment::Right,
paragraph_alignment: Alignment::Right,
..base.clone()
},
},
Section {
label: "Image — Left (baseline)",
text: txt(
"Left-aligned paragraph with an image: \
. \
The image should stay at the left edge."
)
.leak(),
style: MarkdownStyle {
..base.clone()
},
},
Section {
label: "Image — standalone center",
text: txt(
"Text above the centered image.\n\n\
\n\n\
Text below the centered image.",
)
.leak(),
style: MarkdownStyle {
paragraph_alignment: Alignment::Center,
..base.clone()
},
},
];
run(§ions, width)
}
fn main() -> std::io::Result<()> {
crossterm::terminal::enable_raw_mode()?;
crossterm::execute!(stdout(), crossterm::terminal::EnterAlternateScreen)?;
let mut terminal = Terminal::new(ratatui::backend::CrosstermBackend::new(stdout()))?;
let mut scroll: u16 = 0;
#[cfg(feature = "image-protocol")]
let mut state = {
use limner::render_image::Picker;
use std::collections::HashMap;
ImageDemoState {
image_cache: HashMap::new(),
protocol_cache: HashMap::new(),
protocol_clip_cache: HashMap::new(),
picker: Picker::from_query_stdio()
.unwrap_or_else(|_| limner::render_image::halfblock_picker()),
}
};
#[cfg(feature = "image-protocol")]
terminal.draw(|_| {})?;
loop {
let size = terminal.size()?;
let area: Rect = size.into();
let content_width = area.width.saturating_sub(2);
#[allow(unused_mut)]
let (mut lines, images, links) = sections(content_width);
let img_count = images.len();
let link_count = links.len();
#[cfg(feature = "image-protocol")]
let placements = {
for img in &images {
if !state.image_cache.contains_key(&img.url) {
let url = img.url.clone();
if let Some(bytes) = fetch_image(&url) {
use limner::render_image::img_crate;
if let Ok(dyn_img) = img_crate::load_from_memory(&bytes) {
state.image_cache.insert(url, dyn_img);
}
}
}
}
let font_size = state.picker.font_size();
limner::render_image::prepare_inline_images(
&mut lines,
&images,
&state.image_cache,
&mut state.protocol_cache,
&state.picker,
&font_size,
content_width,
10,
)
};
let line_count = lines.len();
let block = Block::default()
.title(" limner alignment demo ")
.borders(Borders::ALL)
.title_bottom(format!(
" {scroll}/{line_count} lines · {img_count} images · {link_count} links ",
));
let inner = block.inner(area);
scroll = scroll.min(line_count.saturating_sub(inner.height as usize) as u16);
terminal.draw(|f| {
f.render_widget(block, area);
f.render_widget(
Paragraph::new(lines.clone())
.wrap(Wrap { trim: false })
.scroll((scroll, 0)),
inner,
);
#[cfg(feature = "image-protocol")]
{
let content_top = inner.y as i32;
let content_bottom = (inner.y + inner.height) as i32;
for p in &placements {
let visual_y = if p.line_start == 0 {
0
} else {
let end = p.line_start.min(lines.len());
Paragraph::new(lines[..end].to_vec())
.wrap(Wrap { trim: false })
.line_count(inner.width)
.max(1) as u16
};
let unclipped_y0 = content_top + visual_y as i32 - scroll as i32;
let mut y0 = unclipped_y0;
let mut y1 = y0 + p.cell_rows as i32;
y0 = y0.max(content_top);
y1 = y1.min(content_bottom);
if y0 >= y1 {
continue;
}
let x = match p.alignment {
Some(RatatuiAlignment::Center) => inner.x
+ (inner.width / 2).saturating_sub(p.cell_cols / 2),
Some(RatatuiAlignment::Right) => {
inner.x + inner.width.saturating_sub(p.cell_cols)
}
_ => inner.x,
};
let visible_height = (y1 - y0) as u16;
let render_protocol = if visible_height < p.cell_rows {
let hidden_top = if unclipped_y0 < content_top {
(content_top - unclipped_y0) as u16
} else {
0
};
let cache_key = (p.url.clone(), visible_height, hidden_top);
state
.protocol_clip_cache
.entry(cache_key)
.or_insert_with(|| {
let Some(img) = state.image_cache.get(&p.url) else {
return state
.protocol_cache
.get(&p.url)
.cloned()
.expect("protocol must exist");
};
limner::render_image::make_scrolled_protocol(
&state.picker,
img,
Size::new(p.cell_cols, p.cell_rows),
Size::new(p.cell_cols, visible_height),
hidden_top,
)
.unwrap_or_else(|| {
state
.protocol_cache
.get(&p.url)
.cloned()
.expect("protocol must exist")
})
})
} else {
state
.protocol_cache
.get(&p.url)
.expect("protocol must exist")
};
f.render_widget(
limner::render_image::Image::new(render_protocol),
Rect {
x,
y: y0 as u16,
width: p.cell_cols,
height: visible_height,
},
);
}
}
})?;
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => break,
KeyCode::Up => scroll = scroll.saturating_sub(1),
KeyCode::Down => scroll = scroll.saturating_add(1),
KeyCode::PageUp => scroll = scroll.saturating_sub(10),
KeyCode::PageDown => scroll = scroll.saturating_add(10),
KeyCode::Home => scroll = 0,
KeyCode::End => scroll = line_count.saturating_sub(1) as u16,
_ => {}
}
}
}
crossterm::terminal::disable_raw_mode()?;
crossterm::execute!(std::io::stdout(), crossterm::terminal::LeaveAlternateScreen)?;
Ok(())
}
#[cfg(feature = "image-protocol")]
struct ImageDemoState {
image_cache: std::collections::HashMap<String, limner::render_image::img_crate::DynamicImage>,
protocol_cache: std::collections::HashMap<String, limner::render_image::Protocol>,
protocol_clip_cache:
std::collections::HashMap<(String, u16, u16), limner::render_image::Protocol>,
picker: limner::render_image::Picker,
}
#[cfg(feature = "image-protocol")]
fn fetch_image(url: &str) -> Option<Vec<u8>> {
use std::io::Read;
let resp = ureq::get(url)
.set("User-Agent", "limner-demo/0.1")
.timeout(std::time::Duration::from_secs(15))
.call()
.ok()?;
let mut reader = resp.into_reader();
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).ok()?;
Some(bytes)
}