Initial commit

This commit is contained in:
kageru 2020-01-23 23:05:40 +01:00
commit 4fa4764cd4
Signed by: kageru
GPG Key ID: 8282A2BEA4ADA3D2
5 changed files with 266 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
**/*.rs.bk

9
Cargo.toml Normal file
View File

@ -0,0 +1,9 @@
[package]
name = "tracc"
version = "0.1.0"
authors = ["kageru <kageru@encode.moe>"]
edition = "2018"
[dependencies]
tui = "0.8.0"
termion = "1.5"

77
src/events/mod.rs Normal file
View File

@ -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<I> {
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<Event<Key>>,
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<Event<Key>, mpsc::RecvError> {
self.rx.recv()
}
}

77
src/main.rs Normal file
View File

@ -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<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(tracc.selected)
.highlight_style(Style::default().fg(Color::LightGreen))
.highlight_symbol(">")
.render(&mut frame, size);
})?;
Ok(())
}

101
src/tracc.rs Normal file
View File

@ -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<Todo>,
pub selected: Option<usize>,
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<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.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<impl Backend>,
) -> 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();
}
}