forked from kageru/tracc
refactor to OOP because I’m too lazy to pass around stuff
This commit is contained in:
parent
3279d63952
commit
904a3dbe2d
74
src/main.rs
74
src/main.rs
|
@ -1,86 +1,18 @@
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::default::Default;
|
|
||||||
use termion::event::Key;
|
|
||||||
use termion::raw::IntoRawMode;
|
use termion::raw::IntoRawMode;
|
||||||
use termion::input::TermRead;
|
|
||||||
use tui::backend::Backend;
|
|
||||||
use tui::backend::TermionBackend;
|
use tui::backend::TermionBackend;
|
||||||
use tui::style::{Color, Style};
|
|
||||||
use tui::widgets::*;
|
|
||||||
use tui::Terminal;
|
use tui::Terminal;
|
||||||
|
mod todolist;
|
||||||
mod tracc;
|
mod tracc;
|
||||||
use tracc::Tracc;
|
use tracc::Tracc;
|
||||||
|
|
||||||
pub enum Mode {
|
|
||||||
Insert,
|
|
||||||
Normal,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<(), io::Error> {
|
fn main() -> Result<(), io::Error> {
|
||||||
let stdout = io::stdout().into_raw_mode()?;
|
let stdout = io::stdout().into_raw_mode()?;
|
||||||
let mut inputs = io::stdin().keys();
|
|
||||||
let backend = TermionBackend::new(stdout);
|
let backend = TermionBackend::new(stdout);
|
||||||
let mut terminal = Terminal::new(backend)?;
|
let mut terminal = Terminal::new(backend)?;
|
||||||
let mut tracc = Tracc::open_or_create();
|
|
||||||
let mut register = std::default::Default::default();
|
|
||||||
terminal.hide_cursor()?;
|
terminal.hide_cursor()?;
|
||||||
terminal.clear()?;
|
terminal.clear()?;
|
||||||
loop {
|
let mut tracc = Tracc::new(terminal);
|
||||||
refresh(&mut terminal, &tracc)?;
|
tracc.run()
|
||||||
// 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(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn refresh(terminal: &mut tui::Terminal<impl Backend>, 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(())
|
|
||||||
}
|
|
||||||
|
|
|
@ -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<Todo>,
|
||||||
|
pub selected: usize,
|
||||||
|
pub register: Option<Todo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<Vec<Todo>> {
|
||||||
|
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<String> {
|
||||||
|
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<Todo> {
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
}
|
203
src/tracc.rs
203
src/tracc.rs
|
@ -1,130 +1,103 @@
|
||||||
use super::Mode;
|
use super::todolist::TodoList;
|
||||||
use serde::{Deserialize, Serialize};
|
use std::default::Default;
|
||||||
use serde_json::from_reader;
|
use std::io;
|
||||||
use std::fs::File;
|
use termion::event::Key;
|
||||||
use std::io::{self, BufReader, Write};
|
use termion::input::TermRead;
|
||||||
use tui::backend::Backend;
|
use tui::backend::TermionBackend;
|
||||||
use tui::Terminal;
|
use tui::style::{Color, Style};
|
||||||
|
use tui::widgets::*;
|
||||||
|
|
||||||
|
type Terminal = tui::Terminal<TermionBackend<termion::raw::RawTerminal<io::Stdout>>>;
|
||||||
|
|
||||||
|
pub enum Mode {
|
||||||
|
Insert,
|
||||||
|
Normal,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Tracc {
|
pub struct Tracc {
|
||||||
// We use owned strings here because they’re easier to manipulate when editing.
|
todos: TodoList,
|
||||||
pub todos: Vec<Todo>,
|
terminal: Terminal,
|
||||||
pub selected: usize,
|
input_mode: Mode,
|
||||||
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<Vec<Todo>> {
|
|
||||||
File::open(JSON_PATH)
|
|
||||||
.ok()
|
|
||||||
.map(|f| BufReader::new(f))
|
|
||||||
.and_then(|r| from_reader(r).ok())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tracc {
|
impl Tracc {
|
||||||
pub fn open_or_create() -> Self {
|
pub fn new(terminal: Terminal) -> Self {
|
||||||
Self {
|
Self {
|
||||||
todos: read_todos().unwrap_or(vec![Todo::new("This is a list entry")]),
|
todos: TodoList::open_or_create(),
|
||||||
selected: 0,
|
terminal,
|
||||||
mode: Mode::Normal,
|
input_mode: Mode::Normal,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn printable_todos(&self) -> Vec<String> {
|
pub fn run(&mut self) -> Result<(), io::Error> {
|
||||||
self.todos
|
let mut inputs = io::stdin().keys();
|
||||||
.iter()
|
loop {
|
||||||
.map(|todo| format!("[{}] {}", if todo.done { 'x' } else { ' ' }, todo.text))
|
refresh(&mut self.terminal, &self.todos)?;
|
||||||
.collect()
|
// I need to find a better way to handle inputs. This is awful.
|
||||||
}
|
let input = inputs.next().unwrap()?;
|
||||||
|
match self.input_mode {
|
||||||
pub fn selection_down(&mut self) {
|
Mode::Normal => match input {
|
||||||
self.selected = (self.selected + 1).min(self.todos.len().saturating_sub(1));
|
Key::Char('q') => {
|
||||||
}
|
self.todos.persist();
|
||||||
|
self.terminal.clear()?;
|
||||||
pub fn selection_up(&mut self) {
|
break;
|
||||||
self.selected = self.selected.saturating_sub(1);
|
}
|
||||||
}
|
Key::Char('j') => self.todos.selection_down(),
|
||||||
|
Key::Char('k') => self.todos.selection_up(),
|
||||||
pub fn insert(&mut self, todo: Todo) {
|
Key::Char('o') => {
|
||||||
if self.selected == self.todos.len().saturating_sub(1) {
|
self.todos.insert(Default::default());
|
||||||
self.todos.push(todo);
|
self.set_mode(Mode::Insert)?;
|
||||||
self.selected = self.todos.len() - 1;
|
}
|
||||||
} else {
|
Key::Char('a') | Key::Char('A') => self.set_mode(Mode::Insert)?,
|
||||||
self.todos.insert(self.selected + 1, todo);
|
Key::Char(' ') => self.todos.toggle_current(),
|
||||||
self.selected += 1;
|
// 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<Todo> {
|
|
||||||
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<impl Backend>,
|
|
||||||
) -> 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn append_to_current(&mut self, chr: char) {
|
fn set_mode(&mut self, mode: Mode) -> Result<(), io::Error> {
|
||||||
self.todos[self.selected].text.push(chr);
|
match mode {
|
||||||
}
|
Mode::Insert => self.terminal.show_cursor()?,
|
||||||
|
Mode::Normal => {
|
||||||
pub fn current_pop(&mut self) {
|
self.todos.normal_mode();
|
||||||
self.todos[self.selected].text.pop();
|
self.terminal.hide_cursor()?
|
||||||
}
|
}
|
||||||
|
};
|
||||||
pub fn persist(self) {
|
self.input_mode = mode;
|
||||||
let string = serde_json::to_string(&self.todos).unwrap();
|
Ok(())
|
||||||
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 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(())
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user