use crossterm::{ cursor::{Hide, MoveTo, Show}, event, event::{Event, KeyCode}, terminal, terminal::{Clear, ClearType}, QueueableCommand, }; use std::fmt::Display; use std::time::Duration; use std::{ io, io::{Stdout, Write}, }; const CURRENT: char = '>'; const NON_CURRENT: char = ' '; enum Action { MoveDown, MoveUp, Select, Exit, } #[derive(Default)] pub struct Entry { title: String, info: Vec<(String, String)>, image: Option, } impl Entry { pub fn new(title: String) -> Self { Self { title, ..Default::default() } } // Making the Entry fields private and adding this method makes it so that data is only added, // not removed. pub fn add_info(&mut self, key: &str, value: T) { self.info.push((key.to_owned(), value.to_string())); } pub fn set_image(&mut self, sixel_data: String) { self.image = Some(sixel_data); } } fn get_input() -> Option { match event::poll(Duration::MAX) { Ok(true) => { let event = event::read(); match event { Ok(Event::Key(k)) => Some(match k.code { KeyCode::Char('j') => Action::MoveDown, KeyCode::Char('k') => Action::MoveUp, KeyCode::Enter => Action::Select, KeyCode::Char('q') => Action::Exit, _ => return None, }), Err(e) => { eprintln!("ERROR: {e:#?}"); exit(); } _ => None, } } Ok(false) => None, Err(e) => { eprintln!("ERROR: {e:#?}"); exit(); } } } fn exit() -> ! { io::stdout().queue(Show).unwrap().flush().unwrap(); terminal::disable_raw_mode().unwrap(); std::process::exit(1); } pub fn select(entries: &[Entry]) -> Result { let (width, height) = terminal::size()?; let mut stdout = io::stdout(); stdout.queue(Hide)?; let mut selected: u16 = 0; let offset = width / 3; let mut should_render = true; loop { if should_render { render(&mut stdout, entries, selected, offset)?; should_render = false; } terminal::enable_raw_mode()?; let input = get_input(); terminal::disable_raw_mode()?; if let Some(m) = input { match m { Action::MoveDown => { if selected <= (height - 1).min((entries.len() - 2) as u16) { selected += 1; should_render = true; } } Action::MoveUp => { if selected < 1 { selected = 0; } else { selected -= 1; should_render = true; } } Action::Select => { stdout .queue(MoveTo(0, 0))? .queue(Clear(ClearType::All))? .queue(Show)? .flush()?; return Ok(selected); } Action::Exit => { stdout .queue(MoveTo(0, 0))? .queue(Clear(ClearType::All))? .queue(Show)? .flush()?; exit(); } } stdout.queue(MoveTo(0, selected))?; } stdout.flush()?; } } fn render( stdout: &mut Stdout, entries: &[Entry], selected: u16, offset: u16, ) -> Result<(), io::Error> { if entries.is_empty() { return Ok(()); } stdout.queue(MoveTo(0, 0))?.queue(Clear(ClearType::All))?; for (i, entry) in entries.iter().enumerate() { stdout .queue(MoveTo(0, i as u16))? .write_all(if i == selected as usize { &[CURRENT as u8] } else { &[NON_CURRENT as u8] })?; stdout.write_all(entry.title.as_bytes())?; } if let Some(sixel_data) = &entries[selected as usize].image { stdout .queue(MoveTo(offset * 2, 0))? .write_all(sixel_data.as_bytes())?; } for (i, line) in entries[selected as usize].info.iter().enumerate() { stdout .queue(MoveTo(offset, i as u16))? .write_all(format!("{}: {}", line.0, line.1).as_bytes())?; } stdout.queue(MoveTo(0, selected))?.flush()?; Ok(()) }