Files
manga-cli/src/select.rs

174 lines
4.6 KiB
Rust

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<String>,
}
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<T: Display>(&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<Action> {
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<u16, std::io::Error> {
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(())
}