320 lines
10 KiB
Rust
320 lines
10 KiB
Rust
#![allow(dead_code, unused)] // todo
|
|
use crate::*;
|
|
|
|
use std::cmp::Ordering::*;
|
|
use std::io;
|
|
|
|
use anyhow::{anyhow, Result};
|
|
|
|
use crossterm::{
|
|
cursor::{self, MoveTo, RestorePosition, SavePosition},
|
|
execute, queue,
|
|
style::{Color, SetBackgroundColor, SetForegroundColor, Stylize},
|
|
terminal::{
|
|
disable_raw_mode, enable_raw_mode, size, Clear, ClearType::All, EnterAlternateScreen,
|
|
LeaveAlternateScreen,
|
|
},
|
|
};
|
|
|
|
/// state of the user interface
|
|
pub struct Ui<T>
|
|
where
|
|
T: io::Write,
|
|
{
|
|
pub active_color_pair: (Color, Color),
|
|
pub inactive_color_pair: (Color, Color),
|
|
pub inactive_done_color_pair: (Color, Color),
|
|
pub header_color_pair: (Color, Color),
|
|
pub ostream: T,
|
|
pub scrolloff: usize,
|
|
pub current_sort_mode: SortMode,
|
|
pub document: Document,
|
|
pub original_document: Document,
|
|
pub width: usize,
|
|
pub save_on_quit: bool,
|
|
pub height: usize,
|
|
pub active_entry_idx: usize,
|
|
pub current_scroll_offset: usize,
|
|
queue_sort_update: bool
|
|
}
|
|
|
|
impl<T> Ui<T>
|
|
where
|
|
T: io::Write,
|
|
{
|
|
/// returns a new `Ui` using `ostream` as its' output stream
|
|
/// and sets up terminal state.
|
|
pub fn init(ostream: T, document: Document) -> Self {
|
|
let (width, height) = size().unwrap();
|
|
let (width, height) = (width as usize, height as usize);
|
|
|
|
let active_color_pair = (Color::Black, Color::Yellow);
|
|
let inactive_color_pair = (Color::Reset, Color::Reset);
|
|
let inactive_done_color_pair = (Color::DarkGrey, Color::Reset);
|
|
let header_color_pair = (Color::Yellow, Color::Reset);
|
|
|
|
let mut ui = Ui {
|
|
active_color_pair,
|
|
save_on_quit: true,
|
|
inactive_color_pair,
|
|
inactive_done_color_pair,
|
|
header_color_pair,
|
|
current_sort_mode: Default,
|
|
active_entry_idx: 0,
|
|
scrolloff: 8,
|
|
current_scroll_offset: 0,
|
|
original_document: document.clone(),
|
|
document,
|
|
ostream,
|
|
width,
|
|
height,
|
|
queue_sort_update: false,
|
|
};
|
|
|
|
queue!(
|
|
ui.ostream,
|
|
SavePosition,
|
|
EnterAlternateScreen,
|
|
Clear(All),
|
|
cursor::Hide
|
|
);
|
|
|
|
enable_raw_mode().unwrap_or_else(|e| {
|
|
Log::error(format!("IO-error when enabling raw-mode: `{}`", e));
|
|
process::exit(ErrorCode::IO.into());
|
|
});
|
|
ui
|
|
}
|
|
|
|
/// resets terminal state that `Ui::init()` sets
|
|
pub fn deinit(&mut self) -> Result<()> {
|
|
disable_raw_mode()?;
|
|
execute!(
|
|
self.ostream,
|
|
LeaveAlternateScreen,
|
|
RestorePosition,
|
|
cursor::Show
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// updates `self.width` and `self.height`.
|
|
/// errs if the new dimensions are too small
|
|
pub fn update_dimensions(&mut self) -> Result<()> {
|
|
let (width, height) = size()?;
|
|
self.width = width as usize;
|
|
self.height = height as usize;
|
|
|
|
if height < 4 || width < 45 {
|
|
Err(anyhow!(
|
|
"ui::update_dimensions: Terminal size too small to display UI"
|
|
))
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub fn clear(&mut self) -> Result<()> {
|
|
queue!(self.ostream, Clear(All), MoveTo(0, 0))?;
|
|
Ok(())
|
|
}
|
|
|
|
/// draws the entire ui including unchanged parts
|
|
pub fn draw(&mut self) -> Result<()> {
|
|
self.update_dimensions()?;
|
|
self.update_scroll_offset();
|
|
self.clear().unwrap();
|
|
|
|
self.apply_sort_mode();
|
|
|
|
self.draw_header();
|
|
for (i, entry) in self
|
|
.document
|
|
.entries
|
|
.iter()
|
|
.skip(self.current_scroll_offset)
|
|
.enumerate()
|
|
{
|
|
if i >= self.inner_height() {
|
|
break;
|
|
}
|
|
let mut bold = false;
|
|
let (fg, bg) = match i == self.active_entry_idx - self.current_scroll_offset {
|
|
true => {
|
|
bold = true;
|
|
self.active_color_pair
|
|
}
|
|
false if entry.done => self.inactive_done_color_pair,
|
|
false => self.inactive_color_pair,
|
|
};
|
|
|
|
queue!(self.ostream, SetForegroundColor(fg), SetBackgroundColor(bg));
|
|
let mut line = String::with_capacity(self.width);
|
|
if entry.done {
|
|
line += " [x] ";
|
|
} else {
|
|
line += " [ ] ";
|
|
}
|
|
|
|
if let Some(deadline) = entry.deadline {
|
|
line += &format!("{}", deadline.format("(%Y-%m-%d %H:%M)"));
|
|
} else {
|
|
line += &" ".repeat("(YYYY-mm-dd HH:MM)".len());
|
|
}
|
|
line += " ";
|
|
|
|
let space = self.width - line.len() - 1;
|
|
let mut text = entry.text.clone();
|
|
if text.chars().count() > space {
|
|
text = text[0..space - 3].to_string() + "... ";
|
|
}
|
|
line += &text;
|
|
let space = self.width - line.len();
|
|
line += &" ".repeat(space);
|
|
|
|
match bold {
|
|
true => write!(self.ostream, "{}\r\n", line.bold()),
|
|
false => write!(self.ostream, "{}\r\n", line),
|
|
};
|
|
queue!(
|
|
self.ostream,
|
|
SetBackgroundColor(Color::Reset),
|
|
SetForegroundColor(Color::Reset)
|
|
);
|
|
}
|
|
|
|
self.ostream.flush()?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn draw_header(&mut self) {
|
|
let (fg, bg) = self.header_color_pair;
|
|
queue!(self.ostream, SetForegroundColor(fg), SetBackgroundColor(bg));
|
|
|
|
let mut line = String::with_capacity(self.width);
|
|
line += " [todue] ";
|
|
line += &" ".repeat(" [x] (YYYY-mm-dd HH:MM) ".len() - line.len());
|
|
line += &self.document.title.clone().unwrap_or("TODO".into());
|
|
let space = self.width - line.len();
|
|
line += &" ".repeat(space);
|
|
write!(self.ostream, "{}\r\n", line);
|
|
write!(self.ostream, "{}\r\n", "—".repeat(self.width));
|
|
queue!(
|
|
self.ostream,
|
|
SetForegroundColor(Color::Reset),
|
|
SetBackgroundColor(Color::Reset)
|
|
);
|
|
}
|
|
|
|
/// update the index of the first *shown* entry using `self.scrolloff`
|
|
pub fn update_scroll_offset(&mut self) {
|
|
if self.current_scroll_offset + self.scrolloff >= self.active_entry_idx {
|
|
let diff = (self.current_scroll_offset + self.scrolloff).abs_diff(self.active_entry_idx);
|
|
self.current_scroll_offset = self.current_scroll_offset.saturating_sub(diff);
|
|
|
|
} else if (self.current_scroll_offset + self.inner_height()).saturating_sub(self.scrolloff) <= self.active_entry_idx {
|
|
let diff = (self.current_scroll_offset + self.inner_height()).saturating_sub(self.scrolloff).abs_diff(self.active_entry_idx);
|
|
self.current_scroll_offset = (self.current_scroll_offset + diff).min(
|
|
self.document
|
|
.entries
|
|
.len()
|
|
.saturating_sub(self.inner_height()),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// get height in characters excluding header/title
|
|
pub fn inner_height(&self) -> usize {
|
|
self.height - 3
|
|
}
|
|
|
|
pub fn move_selection(&mut self, dir: MoveDirection) -> Result<()> {
|
|
match dir {
|
|
Down => {
|
|
self.active_entry_idx = (self.active_entry_idx + 1) % self.document.entries.len()
|
|
}
|
|
Up => {
|
|
self.active_entry_idx = ((self.active_entry_idx as isize - 1)
|
|
.rem_euclid(self.document.entries.len() as isize))
|
|
as usize
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn move_selection_to_bottom(&mut self) {
|
|
self.active_entry_idx = self.document.entries.len() - 1;
|
|
}
|
|
|
|
pub fn move_selection_to_top(&mut self) {
|
|
self.active_entry_idx = 0;
|
|
}
|
|
|
|
pub fn move_selected_entry(&mut self, dir: MoveDirection) {
|
|
let swap_idx = match dir {
|
|
Down => (self.active_entry_idx + 1) % self.document.entries.len(),
|
|
Up => {
|
|
((self.active_entry_idx as isize - 1)
|
|
.rem_euclid(self.document.entries.len() as isize)) as usize
|
|
}
|
|
};
|
|
self.document.entries.swap(self.active_entry_idx, swap_idx);
|
|
self.move_selection(dir);
|
|
}
|
|
|
|
pub fn toggle_active_entry(&mut self) {
|
|
let state = self.document.entries[self.active_entry_idx].done;
|
|
self.document.entries[self.active_entry_idx].done = !state;
|
|
}
|
|
|
|
pub fn cycle_sort_mode(&mut self) {
|
|
self.current_sort_mode = match self.current_sort_mode {
|
|
Default => ByDeadlineDescending,
|
|
ByDeadlineDescending => ByDeadlineAscending,
|
|
ByDeadlineAscending => ByTextAscending,
|
|
ByTextAscending => ByTextDescending,
|
|
ByTextDescending => Default,
|
|
};
|
|
self.queue_sort_update = true;
|
|
}
|
|
|
|
pub fn apply_sort_mode(&mut self) {
|
|
if self.queue_sort_update {
|
|
match self.current_sort_mode {
|
|
Default => self.document = self.original_document.clone(),
|
|
ByDeadlineDescending => {
|
|
self.document.entries.sort_by_key(|entry| entry.deadline);
|
|
self.document.entries.reverse();
|
|
},
|
|
ByDeadlineAscending => self.document.entries.sort_by_key(|entry| entry.deadline),
|
|
ByTextAscending => self.document.entries.sort_by_key(|entry| entry.text.to_lowercase()),
|
|
ByTextDescending => {
|
|
self.document.entries.sort_by_key(|entry| entry.text.to_lowercase());
|
|
self.document.entries.reverse();
|
|
},
|
|
}
|
|
self.queue_sort_update = false;
|
|
}
|
|
}
|
|
|
|
pub fn dont_save_on_quit(&mut self) {
|
|
self.save_on_quit = false;
|
|
Log::info("Quitting without saving file...");
|
|
}
|
|
}
|
|
|
|
pub enum MoveDirection {
|
|
Down,
|
|
Up,
|
|
}
|
|
pub use MoveDirection::*;
|
|
|
|
pub enum SortMode {
|
|
Default,
|
|
ByDeadlineDescending,
|
|
ByDeadlineAscending,
|
|
ByTextAscending,
|
|
ByTextDescending,
|
|
}
|
|
pub use SortMode::*;
|