use std::fmt::Write;
use std::path::PathBuf;
use crate::fmt::{horizontal_border, padding};
use crate::io_error;
use crate::terminal::{UnicodeTerminalFrame, str_cols};
#[derive(Debug)]
pub struct TextPreview {
left_pane: TextPreviewPane,
right_pane: TextPreviewPane,
}
impl TextPreview {
pub fn new(left: Option<TextPreviewPane>, right: Option<TextPreviewPane>) -> Self {
Self {
left_pane: left.unwrap_or_else(TextPreviewPane::hidden),
right_pane: right.unwrap_or_else(TextPreviewPane::hidden),
}
}
pub fn render(&mut self, frame: &mut UnicodeTerminalFrame) -> std::fmt::Result {
self.calculate_pane_regions(frame.size().to_region());
let (position, subframe) = self.render_left_pane()?;
frame.draw(position, &subframe);
let (position, subframe) = self.render_right_pane()?;
frame.draw(position, &subframe);
Ok(())
}
fn calculate_pane_regions(&mut self, region: tuinix::TerminalRegion) {
let pane_region = region.take_bottom(region.size.rows / 3);
if self.left_pane.desired_cols() + self.right_pane.desired_cols() <= pane_region.size.cols {
self.left_pane.region = pane_region
.take_left(self.left_pane.desired_cols())
.take_bottom(self.left_pane.desired_rows());
self.right_pane.region = pane_region
.take_right(self.right_pane.desired_cols())
.take_bottom(self.right_pane.desired_rows());
} else if self.right_pane.is_empty() {
self.left_pane.region = pane_region.take_bottom(self.left_pane.desired_rows());
} else if self.left_pane.is_empty() {
self.right_pane.region = pane_region.take_bottom(self.right_pane.desired_rows());
} else {
self.left_pane.region = pane_region
.take_left(pane_region.size.cols / 2)
.take_bottom(self.left_pane.desired_rows());
self.right_pane.region = pane_region
.take_right(pane_region.size.cols / 2)
.take_bottom(self.right_pane.desired_rows());
}
}
fn render_left_pane(
&self,
) -> Result<(tuinix::TerminalPosition, UnicodeTerminalFrame), std::fmt::Error> {
let region = self.left_pane.region;
let mut frame = UnicodeTerminalFrame::new(region.size);
let cols = region.size.cols;
if self.left_pane.hidden || cols < 2 {
return Ok((region.position, frame));
}
let title = self.left_pane.title();
writeln!(frame, "─{}┐", horizontal_border(title, cols - 2))?;
for _ in 1..region.size.rows {
writeln!(frame, "{}│", padding(' ', cols - 1))?;
}
let text_region = region.size.to_region().drop_top(1).drop_right(1);
let mut text_frame = UnicodeTerminalFrame::new(text_region.size);
self.left_pane.render_text(&mut text_frame)?;
frame.draw(text_region.position, &text_frame);
Ok((region.position, frame))
}
fn render_right_pane(
&self,
) -> Result<(tuinix::TerminalPosition, UnicodeTerminalFrame), std::fmt::Error> {
let region = self.right_pane.region;
let mut frame = UnicodeTerminalFrame::new(region.size);
let cols = region.size.cols;
if self.right_pane.hidden || cols < 2 {
return Ok((region.position, frame));
}
let title = self.right_pane.title();
writeln!(frame, "┌{}─", horizontal_border(title, cols - 2))?;
for _ in 1..region.size.rows {
writeln!(frame, "│")?;
}
let text_region = region.size.to_region().drop_top(1).drop_left(1);
let mut text_frame = UnicodeTerminalFrame::new(text_region.size);
self.right_pane.render_text(&mut text_frame)?;
frame.draw(text_region.position, &text_frame);
Ok((region.position, frame))
}
}
#[derive(Debug)]
pub struct TextPreviewPane {
title: String,
text: String,
max_rows: usize,
max_cols: usize,
region: tuinix::TerminalRegion,
hidden: bool,
}
impl TextPreviewPane {
pub fn new(title: &str, text: &str) -> Self {
let max_rows = text.lines().count();
let max_cols = text.lines().map(str_cols).max().unwrap_or_default();
Self {
title: title.to_owned(),
text: text.to_owned(),
max_rows,
max_cols,
region: tuinix::TerminalRegion::default(),
hidden: false,
}
}
fn hidden() -> Self {
Self {
title: String::new(),
text: String::new(),
max_rows: 0,
max_cols: 0,
region: tuinix::TerminalRegion::default(),
hidden: true,
}
}
fn is_empty(&self) -> bool {
self.max_rows == 0 || self.max_cols == 0
}
fn desired_rows(&self) -> usize {
self.max_rows + 1
}
fn desired_cols(&self) -> usize {
self.max_cols.max(str_cols(&self.title) + 3) + 1
}
fn title(&self) -> &str {
&self.title
}
fn render_text(&self, frame: &mut UnicodeTerminalFrame) -> std::fmt::Result {
for line in self.text.lines().take(frame.size().rows) {
writeln!(frame, "{}", line.trim_end())?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct FilePreviewSpec {
pub left_pane: Option<FilePreviewPaneSpec>,
pub right_pane: Option<FilePreviewPaneSpec>,
}
impl<'text, 'raw> TryFrom<nojson::RawJsonValue<'text, 'raw>> for FilePreviewSpec {
type Error = nojson::JsonParseError;
fn try_from(value: nojson::RawJsonValue<'text, 'raw>) -> Result<Self, Self::Error> {
Ok(Self {
left_pane: value.to_member("left-pane")?.map(TryFrom::try_from)?,
right_pane: value.to_member("right-pane")?.map(TryFrom::try_from)?,
})
}
}
#[derive(Debug, Clone)]
pub struct FilePreviewPaneSpec {
pub file: PathBuf,
}
impl<'text, 'raw> TryFrom<nojson::RawJsonValue<'text, 'raw>> for FilePreviewPaneSpec {
type Error = nojson::JsonParseError;
fn try_from(value: nojson::RawJsonValue<'text, 'raw>) -> Result<Self, Self::Error> {
Ok(Self {
file: value.to_member("file")?.required()?.try_into()?,
})
}
}
#[derive(Debug)]
pub struct FilePreview(TextPreview);
impl FilePreview {
pub fn new(spec: &FilePreviewSpec) -> std::io::Result<Self> {
let left_pane = spec
.left_pane
.as_ref()
.map(Self::load_text_pane)
.transpose()?;
let right_pane = spec
.right_pane
.as_ref()
.map(Self::load_text_pane)
.transpose()?;
Ok(Self(TextPreview::new(left_pane, right_pane)))
}
pub fn render(&mut self, frame: &mut UnicodeTerminalFrame) -> std::fmt::Result {
self.0.render(frame)
}
fn load_text_pane(spec: &FilePreviewPaneSpec) -> std::io::Result<TextPreviewPane> {
let content = if !spec.file.exists() {
Vec::new()
} else {
std::fs::read(&spec.file).map_err(|e| {
io_error(e, &format!("failed to read file '{}'", spec.file.display()))
})?
};
let text = String::from_utf8_lossy(&content).into_owned();
let title = spec
.file
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default();
Ok(TextPreviewPane::new(title, &text))
}
}