use crossterm::{cursor::MoveTo, execute, terminal};
use std::io::{self, Write};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU16, Ordering};
use super::colors::ansi;
const RESERVED_LINES: u16 = 4;
pub mod escape {
pub fn set_scroll_region(top: u16, bottom: u16) -> String {
format!("\x1b[{};{}r", top, bottom)
}
pub const RESET_SCROLL_REGION: &str = "\x1b[r";
pub const SAVE_CURSOR: &str = "\x1b[s";
pub const RESTORE_CURSOR: &str = "\x1b[u";
pub fn move_to_line(line: u16) -> String {
format!("\x1b[{};1H", line)
}
}
#[derive(Debug)]
pub struct LayoutState {
pub active: AtomicBool,
pub term_height: AtomicU16,
pub term_width: AtomicU16,
}
impl Default for LayoutState {
fn default() -> Self {
let (width, height) = terminal::size().unwrap_or((80, 24));
Self {
active: AtomicBool::new(false),
term_height: AtomicU16::new(height),
term_width: AtomicU16::new(width),
}
}
}
impl LayoutState {
pub fn new() -> Arc<Self> {
Arc::new(Self::default())
}
pub fn is_active(&self) -> bool {
self.active.load(Ordering::SeqCst)
}
pub fn height(&self) -> u16 {
self.term_height.load(Ordering::SeqCst)
}
pub fn width(&self) -> u16 {
self.term_width.load(Ordering::SeqCst)
}
pub fn status_line(&self) -> u16 {
self.height().saturating_sub(3)
}
pub fn focus_line(&self) -> u16 {
self.height().saturating_sub(2)
}
pub fn input_line(&self) -> u16 {
self.height().saturating_sub(1)
}
pub fn mode_line(&self) -> u16 {
self.height()
}
}
pub struct TerminalLayout {
state: Arc<LayoutState>,
}
impl TerminalLayout {
pub fn new() -> Self {
Self {
state: LayoutState::new(),
}
}
pub fn state(&self) -> Arc<LayoutState> {
self.state.clone()
}
pub fn init(&self) -> io::Result<()> {
let mut stdout = io::stdout();
let (width, height) = terminal::size()?;
self.state.term_width.store(width, Ordering::SeqCst);
self.state.term_height.store(height, Ordering::SeqCst);
let scroll_bottom = height.saturating_sub(RESERVED_LINES);
execute!(stdout, MoveTo(0, height - 1))?;
for _ in 0..RESERVED_LINES {
println!();
}
print!("{}", escape::set_scroll_region(1, scroll_bottom));
execute!(stdout, MoveTo(0, 0))?;
self.draw_status_line("")?;
self.draw_focus_line(None)?;
self.draw_input_line(false)?;
self.draw_mode_line(false)?;
execute!(stdout, MoveTo(0, 0))?;
self.state.active.store(true, Ordering::SeqCst);
stdout.flush()?;
Ok(())
}
pub fn update_status(&self, content: &str) -> io::Result<()> {
if !self.state.is_active() {
return Ok(());
}
let mut stdout = io::stdout();
let status_line = self.state.status_line();
print!("{}", escape::SAVE_CURSOR);
print!("{}", escape::move_to_line(status_line));
print!("{}", ansi::CLEAR_LINE);
print!("{}", content);
print!("{}", escape::RESTORE_CURSOR);
stdout.flush()?;
Ok(())
}
fn draw_status_line(&self, content: &str) -> io::Result<()> {
let mut stdout = io::stdout();
let status_line = self.state.status_line();
print!("{}", escape::move_to_line(status_line));
print!("{}", ansi::CLEAR_LINE);
if !content.is_empty() {
print!("{}", content);
}
stdout.flush()?;
Ok(())
}
fn draw_focus_line(&self, content: Option<&str>) -> io::Result<()> {
let mut stdout = io::stdout();
let focus_line = self.state.focus_line();
print!("{}", escape::move_to_line(focus_line));
print!("{}", ansi::CLEAR_LINE);
if let Some(text) = content {
print!(
"{}└{} {}{}{}",
ansi::DIM,
ansi::RESET,
ansi::GRAY,
text,
ansi::RESET
);
}
stdout.flush()?;
Ok(())
}
fn draw_input_line(&self, _has_text: bool) -> io::Result<()> {
let mut stdout = io::stdout();
let input_line = self.state.input_line();
print!("{}", escape::move_to_line(input_line));
print!("{}", ansi::CLEAR_LINE);
stdout.flush()?;
Ok(())
}
fn draw_mode_line(&self, plan_mode: bool) -> io::Result<()> {
let mut stdout = io::stdout();
let mode_line = self.state.mode_line();
print!("{}", escape::move_to_line(mode_line));
print!("{}", ansi::CLEAR_LINE);
if plan_mode {
print!(
"{}⏸ plan mode on (shift+tab to switch){}",
ansi::DIM,
ansi::RESET
);
} else {
print!(
"{}▶ standard mode (shift+tab to switch){}",
ansi::DIM,
ansi::RESET
);
}
stdout.flush()?;
Ok(())
}
pub fn update_mode(&self, plan_mode: bool) -> io::Result<()> {
if !self.state.is_active() {
return Ok(());
}
let mut stdout = io::stdout();
print!("{}", escape::SAVE_CURSOR);
self.draw_mode_line(plan_mode)?;
print!("{}", escape::RESTORE_CURSOR);
stdout.flush()?;
Ok(())
}
pub fn position_for_input(&self) -> io::Result<()> {
if !self.state.is_active() {
return Ok(());
}
let mut stdout = io::stdout();
let input_line = self.state.input_line();
print!("{}", escape::move_to_line(input_line));
print!("{}", ansi::CLEAR_LINE);
stdout.flush()?;
Ok(())
}
pub fn position_for_output(&self) -> io::Result<()> {
if !self.state.is_active() {
return Ok(());
}
print!("{}", escape::RESTORE_CURSOR);
io::stdout().flush()?;
Ok(())
}
pub fn cleanup(&self) -> io::Result<()> {
if !self.state.is_active() {
return Ok(());
}
let mut stdout = io::stdout();
print!("{}", escape::RESET_SCROLL_REGION);
let height = self.state.height();
for line in (height - RESERVED_LINES + 1)..=height {
print!("{}", escape::move_to_line(line));
print!("{}", ansi::CLEAR_LINE);
}
execute!(stdout, MoveTo(0, height - 1))?;
print!("{}", ansi::SHOW_CURSOR);
self.state.active.store(false, Ordering::SeqCst);
stdout.flush()?;
Ok(())
}
pub fn handle_resize(&self) -> io::Result<()> {
if !self.state.is_active() {
return Ok(());
}
let (width, height) = terminal::size()?;
self.state.term_width.store(width, Ordering::SeqCst);
self.state.term_height.store(height, Ordering::SeqCst);
let scroll_bottom = height.saturating_sub(RESERVED_LINES);
print!("{}", escape::set_scroll_region(1, scroll_bottom));
self.draw_status_line("")?;
self.draw_focus_line(None)?;
self.draw_input_line(false)?;
self.draw_mode_line(false)?;
io::stdout().flush()?;
Ok(())
}
}
impl Default for TerminalLayout {
fn default() -> Self {
Self::new()
}
}
impl Drop for TerminalLayout {
fn drop(&mut self) {
let _ = self.cleanup();
}
}
pub fn print_to_scroll_region(content: &str) {
print!("{}", content);
let _ = io::stdout().flush();
}
pub fn println_to_scroll_region(content: &str) {
println!("{}", content);
let _ = io::stdout().flush();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_layout_state_defaults() {
let state = LayoutState::default();
assert!(!state.is_active());
assert!(state.height() > 0);
assert!(state.width() > 0);
}
#[test]
fn test_scroll_region_escape() {
assert_eq!(escape::set_scroll_region(1, 20), "\x1b[1;20r");
assert_eq!(escape::move_to_line(5), "\x1b[5;1H");
}
}