From 4fa4764cd4fe3e0b302241784ea5c55d50729fdf Mon Sep 17 00:00:00 2001 From: kageru Date: Thu, 23 Jan 2020 23:05:40 +0100 Subject: [PATCH] Initial commit --- .gitignore | 2 + Cargo.toml | 9 +++++ src/events/mod.rs | 77 +++++++++++++++++++++++++++++++++++ src/main.rs | 77 +++++++++++++++++++++++++++++++++++ src/tracc.rs | 101 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 266 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/events/mod.rs create mode 100644 src/main.rs create mode 100644 src/tracc.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53eaa21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +**/*.rs.bk diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e9b5503 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "tracc" +version = "0.1.0" +authors = ["kageru "] +edition = "2018" + +[dependencies] +tui = "0.8.0" +termion = "1.5" diff --git a/src/events/mod.rs b/src/events/mod.rs new file mode 100644 index 0000000..fcf8829 --- /dev/null +++ b/src/events/mod.rs @@ -0,0 +1,77 @@ +// This file was copied from https://github.com/fdehau/tui-rs/blob/master/examples/util/event.rs +use std::io; +use std::sync::mpsc; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; +use std::thread; +use std::time::Duration; + +use termion::event::Key; +use termion::input::TermRead; + +pub enum Event { + Input(I), +} + +/// A small event handler that wrap termion input and tick events. Each event +/// type is handled in its own thread and returned to a common `Receiver` +pub struct Events { + rx: mpsc::Receiver>, + input_handle: thread::JoinHandle<()>, +} + +#[derive(Debug, Clone, Copy)] +pub struct Config { + pub exit_key: Key, + pub tick_rate: Duration, +} + +impl Default for Config { + fn default() -> Config { + Config { + exit_key: Key::Char('q'), + tick_rate: Duration::from_millis(250), + } + } +} + +impl Events { + pub fn new() -> Events { + Events::with_config(Config::default()) + } + + pub fn with_config(config: Config) -> Events { + let (tx, rx) = mpsc::channel(); + let ignore_exit_key = Arc::new(AtomicBool::new(false)); + let input_handle = { + let tx = tx.clone(); + let ignore_exit_key = ignore_exit_key.clone(); + thread::spawn(move || { + let stdin = io::stdin(); + for evt in stdin.keys() { + match evt { + Ok(key) => { + if let Err(_) = tx.send(Event::Input(key)) { + return; + } + if !ignore_exit_key.load(Ordering::Relaxed) && key == config.exit_key { + return; + } + } + Err(_) => {} + } + } + }) + }; + Events { + rx, + input_handle, + } + } + + pub fn next(&self) -> Result, mpsc::RecvError> { + self.rx.recv() + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..db1c004 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,77 @@ +use std::io; +use termion::event::Key; +use termion::raw::IntoRawMode; +use tui::backend::Backend; +use tui::backend::TermionBackend; +use tui::style::{Color, Style}; +use tui::widgets::*; +use tui::Terminal; +mod events; +use events::{Event, Events}; +mod tracc; +use tracc::Tracc; + +pub enum Mode { + Insert, + Normal, +} + +fn main() -> Result<(), io::Error> { + let stdout = io::stdout().into_raw_mode()?; + let backend = TermionBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + let mut tracc = Tracc::new(); + terminal.hide_cursor()?; + terminal.clear()?; + let events = Events::new(); + loop { + refresh(&mut terminal, &tracc)?; + // I need to find a better way to handle inputs. This is awful. + match events.next().expect("input ded?") { + Event::Input(input) => match tracc.mode { + Mode::Normal => match input { + Key::Char('q') => break, + Key::Char('j') => tracc.selection_down(), + Key::Char('k') => tracc.selection_up(), + Key::Char('o') => { + tracc.insert(); + 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 Event::Input(Key::Char('d')) = events.next().unwrap() { + tracc.remove_current() + } + } + _ => (), + }, + 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, 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(tracc.selected) + .highlight_style(Style::default().fg(Color::LightGreen)) + .highlight_symbol(">") + .render(&mut frame, size); + })?; + Ok(()) +} diff --git a/src/tracc.rs b/src/tracc.rs new file mode 100644 index 0000000..d5b7018 --- /dev/null +++ b/src/tracc.rs @@ -0,0 +1,101 @@ +use super::Mode; +use std::io; +use tui::backend::Backend; +use tui::Terminal; + +pub struct Tracc { + // We use owned strings here because they’re easier to manipulate when editing. + pub todos: Vec, + pub selected: Option, + pub mode: Mode, +} + +pub struct Todo { + text: String, + done: bool, +} + +impl Todo { + pub fn new(text: &str) -> Self { + Todo { + text: text.to_owned(), + done: false, + } + } +} + +impl Tracc { + pub fn new() -> Self { + Self { + todos: vec![ + Todo::new("This is a list entry"), + Todo::new("a second todo"), + Todo::new("And a third"), + ], + selected: Some(0), + 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.map(|i| (i + 1).min(self.todos.len() - 1)); + } + + pub fn selection_up(&mut self) { + self.selected = self.selected.map(|i| i.saturating_sub(1)); + } + + pub fn insert(&mut self) { + self.todos.insert(self.selected.unwrap() + 1, Todo::new("")); + self.selected = self.selected.map(|n| n + 1); + self.mode = Mode::Normal; + } + + pub fn remove_current(&mut self) { + if let Some(n) = self.selected { + self.todos.remove(n); + self.selected = Some(n.min(self.todos.len() - 1)); + } + } + + pub fn toggle_current(&mut self) { + self.current().done = !self.current().done; + } + + fn current(&mut self) -> &mut Todo { + &mut self.todos[self.selected.unwrap()] + } + + 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() + } + term.hide_cursor()? + }, + }; + self.mode = mode; + Ok(()) + } + + pub fn append_to_current(&mut self, chr: char) { + self.todos[self.selected.unwrap()].text.push(chr); + } + + pub fn current_pop(&mut self) { + self.todos[self.selected.unwrap()].text.pop(); + } +}