use std::io::stdout;
use crossterm::event::{self, Event, KeyCode};
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()
},
},
];
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(),
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 line_count = lines.len();
scroll = scroll.min(line_count.saturating_sub(1) as u16);
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 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);
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 Some(protocol) = state.protocol_cache.get(&p.url) else {
continue;
};
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 y0 = content_top + visual_y as i32 - scroll as i32;
let y1 = y0 + p.cell_rows as i32;
if y0 < content_top || y1 > content_bottom {
continue;
}
f.render_widget(
limner::render_image::Image::new(protocol),
Rect {
x: inner.x,
y: y0 as u16,
width: p.cell_cols,
height: p.cell_rows,
},
);
}
}
})?;
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>,
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)
}