diff --git a/src/main.rs b/src/main.rs index 1ab259c..05c8ca1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,86 +1,18 @@ use std::io; -use std::default::Default; -use termion::event::Key; use termion::raw::IntoRawMode; -use termion::input::TermRead; -use tui::backend::Backend; use tui::backend::TermionBackend; -use tui::style::{Color, Style}; -use tui::widgets::*; use tui::Terminal; +mod todolist; mod tracc; use tracc::Tracc; -pub enum Mode { - Insert, - Normal, -} - fn main() -> Result<(), io::Error> { let stdout = io::stdout().into_raw_mode()?; - let mut inputs = io::stdin().keys(); let backend = TermionBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - let mut tracc = Tracc::open_or_create(); - let mut register = std::default::Default::default(); terminal.hide_cursor()?; terminal.clear()?; - loop { - refresh(&mut terminal, &tracc)?; - // I need to find a better way to handle inputs. This is awful. - let input = inputs.next().unwrap()?; - match tracc.mode { - Mode::Normal => match input { - Key::Char('q') => { - tracc.persist(); - terminal.clear()?; - break; - }, - Key::Char('j') => tracc.selection_down(), - Key::Char('k') => tracc.selection_up(), - Key::Char('o') => { - tracc.insert(Default::default()); - tracc.set_mode(Mode::Insert, &mut terminal)?; - } - Key::Char('a') => tracc.set_mode(Mode::Insert, &mut terminal)?, - Key::Char('A') => tracc.set_mode(Mode::Insert, &mut terminal)?, - Key::Char(' ') => tracc.toggle_current(), - // dd - Key::Char('d') => { - if let Key::Char('d') = inputs.next().unwrap()? { - register = tracc.remove_current() - } - } - Key::Char('p') => { - if register.is_some() { - tracc.insert(register.clone().unwrap()); - } - } - _ => (), - }, - Mode::Insert => match input { - Key::Char('\n') => tracc.set_mode(Mode::Normal, &mut terminal)?, - Key::Esc => tracc.set_mode(Mode::Normal, &mut terminal)?, - Key::Backspace => tracc.current_pop(), - Key::Char(x) => tracc.append_to_current(x), - _ => (), - }, - }; - } - Ok(()) + let mut tracc = Tracc::new(terminal); + tracc.run() } -fn refresh(terminal: &mut tui::Terminal, tracc: &Tracc) -> Result<(), io::Error> { - terminal.draw(|mut frame| { - let size = frame.size(); - let block = Block::default().title(" t r a c c ").borders(Borders::ALL); - SelectableList::default() - .block(block) - .items(&tracc.printable_todos()) - .select(Some(tracc.selected)) - .highlight_style(Style::default().fg(Color::LightGreen)) - .highlight_symbol(">") - .render(&mut frame, size); - })?; - Ok(()) -} diff --git a/src/todolist.rs b/src/todolist.rs new file mode 100644 index 0000000..dba3ee1 --- /dev/null +++ b/src/todolist.rs @@ -0,0 +1,114 @@ +use serde::{Deserialize, Serialize}; +use serde_json::from_reader; +use std::fs::File; +use std::io::{BufReader, Write}; + +pub struct TodoList { + // We use owned strings here because they’re easier to manipulate when editing. + pub todos: Vec, + pub selected: usize, + pub register: Option, +} + +#[derive(Serialize, Deserialize, Default, Clone)] +pub struct Todo { + text: String, + done: bool, +} + +impl Todo { + pub fn new(text: &str) -> Self { + Todo { + text: text.to_owned(), + done: false, + } + } +} + +const JSON_PATH: &str = "tracc.json"; + +fn read_todos() -> Option> { + File::open(JSON_PATH) + .ok() + .map(|f| BufReader::new(f)) + .and_then(|r| from_reader(r).ok()) +} + +impl TodoList { + pub fn open_or_create() -> Self { + TodoList { + todos: read_todos().unwrap_or(vec![Todo::new("This is a list entry")]), + selected: 0, + register: None, + } + } + + pub fn printable_todos(&self) -> Vec { + self.todos + .iter() + .map(|todo| format!("[{}] {}", if todo.done { 'x' } else { ' ' }, todo.text)) + .collect() + } + + pub fn selection_down(&mut self) { + self.selected = (self.selected + 1).min(self.todos.len().saturating_sub(1)); + } + + pub fn selection_up(&mut self) { + self.selected = self.selected.saturating_sub(1); + } + + pub fn insert(&mut self, todo: Todo) { + if self.selected == self.todos.len().saturating_sub(1) { + self.todos.push(todo); + self.selected = self.todos.len() - 1; + } else { + self.todos.insert(self.selected + 1, todo); + self.selected += 1; + } + } + + pub fn remove_current(&mut self) -> Option { + if self.todos.is_empty() { + return None; + } + let index = self.selected; + self.selected = index.min(self.todos.len() - 2); + return Some(self.todos.remove(index)); + } + + pub fn toggle_current(&mut self) { + self.todos[self.selected].done = !self.todos[self.selected].done; + } + + fn current(&self) -> &Todo { + &self.todos[self.selected] + } + + pub fn normal_mode(&mut self) { + if self.current().text.is_empty() { + self.remove_current(); + self.selected = self.selected.saturating_sub(1); + } + } + + pub fn append_to_current(&mut self, chr: char) { + self.todos[self.selected].text.push(chr); + } + + pub fn current_pop(&mut self) { + self.todos[self.selected].text.pop(); + } + + pub fn persist(&self) { + let string = serde_json::to_string(&self.todos).unwrap(); + std::fs::OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(JSON_PATH) + .ok() + .or_else(|| panic!("Can’t save todos to JSON. Dumping raw data:\n{}", string)) + .map(|mut f| f.write(string.as_bytes())); + } +} diff --git a/src/tracc.rs b/src/tracc.rs index aaeaf4f..a32692a 100644 --- a/src/tracc.rs +++ b/src/tracc.rs @@ -1,130 +1,103 @@ -use super::Mode; -use serde::{Deserialize, Serialize}; -use serde_json::from_reader; -use std::fs::File; -use std::io::{self, BufReader, Write}; -use tui::backend::Backend; -use tui::Terminal; +use super::todolist::TodoList; +use std::default::Default; +use std::io; +use termion::event::Key; +use termion::input::TermRead; +use tui::backend::TermionBackend; +use tui::style::{Color, Style}; +use tui::widgets::*; + +type Terminal = tui::Terminal>>; + +pub enum Mode { + Insert, + Normal, +} pub struct Tracc { - // We use owned strings here because they’re easier to manipulate when editing. - pub todos: Vec, - pub selected: usize, - pub mode: Mode, -} - -#[derive(Serialize, Deserialize, Default, Clone)] -pub struct Todo { - text: String, - done: bool, -} - -impl Todo { - pub fn new(text: &str) -> Self { - Todo { - text: text.to_owned(), - done: false, - } - } -} - -const JSON_PATH: &str = "tracc.json"; - -fn read_todos() -> Option> { - File::open(JSON_PATH) - .ok() - .map(|f| BufReader::new(f)) - .and_then(|r| from_reader(r).ok()) + todos: TodoList, + terminal: Terminal, + input_mode: Mode, } impl Tracc { - pub fn open_or_create() -> Self { + pub fn new(terminal: Terminal) -> Self { Self { - todos: read_todos().unwrap_or(vec![Todo::new("This is a list entry")]), - selected: 0, - mode: Mode::Normal, + todos: TodoList::open_or_create(), + terminal, + input_mode: Mode::Normal, } } - pub fn printable_todos(&self) -> Vec { - self.todos - .iter() - .map(|todo| format!("[{}] {}", if todo.done { 'x' } else { ' ' }, todo.text)) - .collect() - } - - pub fn selection_down(&mut self) { - self.selected = (self.selected + 1).min(self.todos.len().saturating_sub(1)); - } - - pub fn selection_up(&mut self) { - self.selected = self.selected.saturating_sub(1); - } - - pub fn insert(&mut self, todo: Todo) { - if self.selected == self.todos.len().saturating_sub(1) { - self.todos.push(todo); - self.selected = self.todos.len() - 1; - } else { - self.todos.insert(self.selected + 1, todo); - self.selected += 1; + pub fn run(&mut self) -> Result<(), io::Error> { + let mut inputs = io::stdin().keys(); + loop { + refresh(&mut self.terminal, &self.todos)?; + // I need to find a better way to handle inputs. This is awful. + let input = inputs.next().unwrap()?; + match self.input_mode { + Mode::Normal => match input { + Key::Char('q') => { + self.todos.persist(); + self.terminal.clear()?; + break; + } + Key::Char('j') => self.todos.selection_down(), + Key::Char('k') => self.todos.selection_up(), + Key::Char('o') => { + self.todos.insert(Default::default()); + self.set_mode(Mode::Insert)?; + } + Key::Char('a') | Key::Char('A') => self.set_mode(Mode::Insert)?, + Key::Char(' ') => self.todos.toggle_current(), + // dd + Key::Char('d') => { + if let Key::Char('d') = inputs.next().unwrap()? { + self.todos.register = self.todos.remove_current() + } + } + Key::Char('p') => { + if self.todos.register.is_some() { + self.todos.insert(self.todos.register.clone().unwrap()); + } + } + _ => (), + }, + Mode::Insert => match input { + Key::Char('\n') | Key::Esc => self.set_mode(Mode::Normal)?, + Key::Backspace => self.todos.current_pop(), + Key::Char(x) => self.todos.append_to_current(x), + _ => (), + }, + }; } - self.mode = Mode::Normal; - } - - pub fn remove_current(&mut self) -> Option { - if self.todos.is_empty() { - return None; - } - let index = self.selected; - self.selected = index.min(self.todos.len() - 1); - return Some(self.todos.remove(index)); - } - - pub fn toggle_current(&mut self) { - self.todos[self.selected].done = !self.todos[self.selected].done; - } - - fn current(&self) -> &Todo { - &self.todos[self.selected] - } - - pub fn set_mode( - &mut self, - mode: Mode, - term: &mut Terminal, - ) -> Result<(), io::Error> { - match mode { - Mode::Insert => term.show_cursor()?, - Mode::Normal => { - if self.current().text.is_empty() { - self.remove_current(); - self.selected = self.selected.saturating_sub(1); - } - term.hide_cursor()? - } - }; - self.mode = mode; Ok(()) } - pub fn append_to_current(&mut self, chr: char) { - self.todos[self.selected].text.push(chr); - } - - pub fn current_pop(&mut self) { - self.todos[self.selected].text.pop(); - } - - pub fn persist(self) { - let string = serde_json::to_string(&self.todos).unwrap(); - std::fs::OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(JSON_PATH) - .ok() - .or_else(|| panic!("Can’t save todos to JSON. Dumping raw data:\n{}", string)) - .map(|mut f| f.write(string.as_bytes())); + fn set_mode(&mut self, mode: Mode) -> Result<(), io::Error> { + match mode { + Mode::Insert => self.terminal.show_cursor()?, + Mode::Normal => { + self.todos.normal_mode(); + self.terminal.hide_cursor()? + } + }; + self.input_mode = mode; + Ok(()) } } + +fn refresh(terminal: &mut Terminal, todos: &TodoList) -> Result<(), io::Error> { + terminal.draw(|mut frame| { + let size = frame.size(); + let block = Block::default().title(" t r a c c ").borders(Borders::ALL); + SelectableList::default() + .block(block) + .items(&todos.printable_todos()) + .select(Some(todos.selected)) + .highlight_style(Style::default().fg(Color::LightGreen)) + .highlight_symbol(">") + .render(&mut frame, size); + })?; + Ok(()) +}