lowfi/src/ui/interface/window.rs
Tal 4891e626e1
feat: logging (#127)
* feat: add basic logging for error codes on tracks

* fix: refine log printing to make sure nothing overlaps

* docs: add comments for logger
2026-04-17 20:23:22 +02:00

130 lines
4.2 KiB
Rust

use crate::ui::{self, interface::TitleBar};
use crossterm::{
cursor::{MoveToColumn, MoveUp},
style::{Print, Stylize as _},
terminal::{Clear, ClearType},
};
use std::fmt::Write as _;
use unicode_segmentation::UnicodeSegmentation as _;
/// Represents an abstraction for drawing the actual lowfi window itself.
///
/// The main purpose of this struct is just to add the fancy border,
/// as well as clear the screen before drawing.
pub struct Window {
/// Whether or not to include borders in the output.
borderless: bool,
/// The titlebar of this window.
pub titlebar: TitleBar,
/// The status (bottom) bar of the window, which for now shouldn't change since initialization.
pub(crate) statusbar: String,
/// The inner width of the window.
width: usize,
/// Whether content items should be automatically padded (spaced).
spaced: bool,
/// Whether to cautiously handle ANSI sequences by adding [`style::Attribute::Reset`] generously.
fancy: bool,
}
impl Window {
/// Initializes a new [Window].
///
/// * `width` - Inner width of the window.
/// * `borderless` - Whether to include borders in the window, or not.
pub fn new(width: usize, borderless: bool, spaced: bool, fancy: bool) -> Self {
let statusbar = if borderless {
String::new()
} else {
let middle = "".repeat(width + 2);
format!("{middle}")
};
Self {
spaced,
statusbar,
borderless,
width,
fancy,
titlebar: TitleBar::new(width, borderless),
}
}
/// Renders the window itself, but doesn't actually draw it.
///
/// `testing` just determines whether to add special features
/// like color resets and carriage returns.
///
/// This returns both the final rendered window and also the full
/// height of the rendered window.
pub(crate) fn render(&self, content: Vec<String>) -> ui::Result<(String, u16)> {
let newline: &str = if self.fancy { "\r\n" } else { "\n" };
let len: u16 = content.len().try_into()?;
// Note that this will have a trailing newline, which we use later.
let menu: String = content.into_iter().fold(String::new(), |mut output, x| {
// Horizontal Padding & Border
let padding = if self.borderless { " " } else { "" };
let space = if self.spaced {
" ".repeat(self.width.saturating_sub(x.graphemes(true).count()))
} else {
String::new()
};
let center = if self.fancy { x.reset().to_string() } else { x };
write!(output, "{padding} {center}{space} {padding}{newline}").unwrap();
output
});
// We're doing this because Windows is stupid and can't stand
// writing to the last line repeatedly.
#[cfg(windows)]
let (height, suffix) = (len + 3, newline);
#[cfg(not(windows))]
let (height, suffix) = (len + 2, "");
// There's no need for another newline after the main menu content, because it already has one.
Ok((
format!(
"{}{newline}{menu}{}{suffix}",
self.titlebar.content, self.statusbar,
),
height,
))
}
/// Actually draws the window, with each element in `content` being on a new line.
///
/// If `log` is [`Some`], then it will also print it after clearing, but before the lowfi window.
pub fn draw(
&mut self,
mut writer: impl std::io::Write,
log: Option<String>,
content: Vec<String>,
) -> ui::Result<()> {
let (rendered, height) = self.render(content)?;
crossterm::queue!(writer, Clear(ClearType::FromCursorDown), MoveToColumn(0))?;
if let Some(log) = log {
crossterm::queue!(writer, Print(log), Print("\n"), MoveToColumn(0))?;
}
crossterm::queue!(
writer,
Clear(ClearType::FromCursorDown),
MoveToColumn(0),
Print(rendered),
MoveToColumn(0),
MoveUp(height - 1),
)?;
writer.flush()?;
Ok(())
}
}