2020-04-19 19:04:27 +02:00
use super ::timesheet ::TimeSheet ;
2020-01-25 12:11:19 +01:00
use super ::todolist ::TodoList ;
use std ::default ::Default ;
2020-04-13 23:56:25 +02:00
use std ::fmt ;
2020-01-25 18:05:35 +01:00
use std ::io ::{ self , Write } ;
2020-01-25 12:11:19 +01:00
use termion ::event ::Key ;
use termion ::input ::TermRead ;
use tui ::backend ::TermionBackend ;
2020-01-25 18:05:35 +01:00
use tui ::layout ::* ;
2020-01-25 12:11:19 +01:00
use tui ::style ::{ Color , Style } ;
use tui ::widgets ::* ;
type Terminal = tui ::Terminal < TermionBackend < termion ::raw ::RawTerminal < io ::Stdout > > > ;
2020-04-13 23:56:25 +02:00
const JSON_PATH_TIME : & str = " tracc_time.json " ;
const JSON_PATH_TODO : & str = " tracc_todo.json " ;
2020-01-25 12:11:19 +01:00
pub enum Mode {
Insert ,
Normal ,
2020-01-23 23:05:40 +01:00
}
2020-01-25 22:15:41 +01:00
#[ derive(PartialEq) ]
enum Focus {
Top ,
Bottom ,
}
2020-01-25 12:11:19 +01:00
pub struct Tracc {
todos : TodoList ,
2020-01-25 22:15:41 +01:00
times : TimeSheet ,
2020-01-25 12:11:19 +01:00
terminal : Terminal ,
input_mode : Mode ,
2020-01-25 22:15:41 +01:00
focus : Focus ,
2020-01-24 00:02:32 +01:00
}
2020-01-23 23:05:40 +01:00
impl Tracc {
2020-01-25 12:11:19 +01:00
pub fn new ( terminal : Terminal ) -> Self {
2020-01-23 23:05:40 +01:00
Self {
2020-04-13 23:56:25 +02:00
todos : TodoList ::open_or_create ( JSON_PATH_TODO ) ,
times : TimeSheet ::open_or_create ( JSON_PATH_TIME ) ,
2020-01-25 12:11:19 +01:00
terminal ,
input_mode : Mode ::Normal ,
2020-01-25 22:15:41 +01:00
focus : Focus ::Top ,
2020-01-24 11:07:37 +01:00
}
2020-01-23 23:05:40 +01:00
}
2020-01-25 12:11:19 +01:00
pub fn run ( & mut self ) -> Result < ( ) , io ::Error > {
2020-04-18 18:15:23 +02:00
macro_rules ! with_focused {
( $action : expr $(, $arg : expr ) * ) = > {
match self . focus {
Focus ::Top = > $action ( & mut self . todos , $( $arg , ) * ) ,
Focus ::Bottom = > $action ( & mut self . times , $( $arg , ) * ) ,
}
} ;
} ;
2020-01-25 12:11:19 +01:00
let mut inputs = io ::stdin ( ) . keys ( ) ;
loop {
2020-01-25 22:15:41 +01:00
self . refresh ( ) ? ;
2020-01-25 12:11:19 +01:00
// 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 {
2020-01-25 12:22:36 +01:00
Key ::Char ( 'q' ) = > break ,
2020-04-18 18:15:23 +02:00
Key ::Char ( 'j' ) = > with_focused! ( ListView ::selection_down ) ,
Key ::Char ( 'k' ) = > with_focused! ( ListView ::selection_up ) ,
2020-01-25 12:11:19 +01:00
Key ::Char ( 'o' ) = > {
2020-04-18 18:15:23 +02:00
with_focused! ( ListView ::insert , Default ::default ( ) , None ) ;
2020-01-25 12:11:19 +01:00
self . set_mode ( Mode ::Insert ) ? ;
}
Key ::Char ( 'a' ) | Key ::Char ( 'A' ) = > self . set_mode ( Mode ::Insert ) ? ,
2020-04-18 18:15:23 +02:00
Key ::Char ( ' ' ) = > with_focused! ( ListView ::toggle_current ) ,
2020-04-19 19:04:27 +02:00
Key ::Char ( 'd' ) = > with_focused! ( ListView ::remove_current ) ,
2020-04-18 18:15:23 +02:00
Key ::Char ( 'p' ) = > with_focused! ( ListView ::paste ) ,
2020-01-25 22:15:41 +01:00
Key ::Char ( '\t' ) = > {
self . focus = match self . focus {
Focus ::Top = > Focus ::Bottom ,
Focus ::Bottom = > Focus ::Top ,
}
}
2020-01-25 12:11:19 +01:00
_ = > ( ) ,
} ,
Mode ::Insert = > match input {
Key ::Char ( '\n' ) | Key ::Esc = > self . set_mode ( Mode ::Normal ) ? ,
2020-04-18 18:15:23 +02:00
Key ::Backspace = > with_focused! ( ListView ::backspace ) ,
Key ::Char ( x ) = > with_focused! ( ListView ::append_to_current , x ) ,
2020-01-25 12:11:19 +01:00
_ = > ( ) ,
} ,
} ;
2020-01-24 11:07:37 +01:00
}
2020-01-25 12:22:36 +01:00
self . terminal . clear ( ) ? ;
2020-04-13 23:56:25 +02:00
persist_state ( & self . todos , & self . times ) ;
2020-01-25 12:11:19 +01:00
Ok ( ( ) )
2020-01-23 23:05:40 +01:00
}
2020-01-25 12:11:19 +01:00
fn set_mode ( & mut self , mode : Mode ) -> Result < ( ) , io ::Error > {
2020-01-23 23:05:40 +01:00
match mode {
2020-01-25 12:11:19 +01:00
Mode ::Insert = > self . terminal . show_cursor ( ) ? ,
2020-01-23 23:05:40 +01:00
Mode ::Normal = > {
2020-01-25 12:11:19 +01:00
self . todos . normal_mode ( ) ;
2020-04-13 23:56:25 +02:00
self . times . normal_mode ( ) ;
2020-01-25 12:11:19 +01:00
self . terminal . hide_cursor ( ) ?
2020-01-24 00:02:32 +01:00
}
2020-01-23 23:05:40 +01:00
} ;
2020-01-25 12:11:19 +01:00
self . input_mode = mode ;
2020-01-23 23:05:40 +01:00
Ok ( ( ) )
}
2020-01-25 22:15:41 +01:00
fn refresh ( & mut self ) -> Result < ( ) , io ::Error > {
fn selectable_list < ' a , C : AsRef < str > > (
title : & ' a str ,
content : & ' a [ C ] ,
selected : Option < usize > ,
) -> SelectableList < ' a > {
SelectableList ::default ( )
. block (
Block ::default ( )
. title ( title )
. borders ( Borders ::TOP | Borders ::RIGHT | Borders ::LEFT ) ,
)
. items ( content )
. select ( selected . into ( ) )
. highlight_style ( Style ::default ( ) . fg ( Color ::LightGreen ) )
. highlight_symbol ( " > " )
}
let printable_todos = self . todos . printable ( ) ;
let top_focus = Some ( self . todos . selected ) . filter ( | _ | self . focus = = Focus ::Top ) ;
let printable_times = self . times . printable ( ) ;
let bottom_focus = Some ( self . times . selected ) . filter ( | _ | self . focus = = Focus ::Bottom ) ;
2020-04-13 23:56:25 +02:00
let total_time = self . times . sum_as_str ( ) ;
let times = self . times . time_by_tasks ( ) ;
2020-01-25 18:05:35 +01:00
2020-01-25 22:15:41 +01:00
self . terminal . draw ( | mut frame | {
let size = frame . size ( ) ;
let chunks = Layout ::default ( )
. direction ( Direction ::Vertical )
. constraints (
[
2020-04-19 19:04:27 +02:00
Constraint ::Percentage ( 40 ) ,
Constraint ::Percentage ( 40 ) ,
Constraint ::Percentage ( 20 ) ,
2020-01-25 22:15:41 +01:00
]
. as_ref ( ) ,
)
. split ( size ) ;
selectable_list ( " t r a c c " , & printable_todos , top_focus )
. render ( & mut frame , chunks [ 0 ] ) ;
2020-04-13 23:56:25 +02:00
selectable_list ( " 🕑 " , & printable_times , bottom_focus ) . render ( & mut frame , chunks [ 1 ] ) ;
Paragraph ::new (
2020-04-19 19:04:27 +02:00
[
Text ::raw ( format! ( " Sum for today: {} \n " , total_time ) ) ,
Text ::raw ( times )
]
2020-04-13 23:56:25 +02:00
. iter ( ) ,
)
2020-04-19 19:04:27 +02:00
. wrap ( true )
2020-04-13 23:56:25 +02:00
. block ( Block ::default ( ) . borders ( Borders ::ALL ) )
. render ( & mut frame , chunks [ 2 ] ) ;
2020-01-25 22:15:41 +01:00
} ) ? ;
Ok ( ( ) )
}
2020-01-23 23:05:40 +01:00
}
2020-01-25 12:22:36 +01:00
2020-04-13 23:56:25 +02:00
fn persist_state ( todos : & TodoList , times : & TimeSheet ) {
fn write ( path : & str , content : String ) {
std ::fs ::OpenOptions ::new ( )
. create ( true )
. write ( true )
. truncate ( true )
. open ( path )
. ok ( )
. or_else ( | | panic! ( " Can’t save state to JSON. Dumping raw data: \n {} " , content ) )
. map ( | mut f | f . write ( content . as_bytes ( ) ) ) ;
}
let todos = serde_json ::to_string ( & todos . todos ) . unwrap ( ) ;
write ( JSON_PATH_TODO , todos ) ;
let times = serde_json ::to_string ( & times . times ) . unwrap ( ) ;
write ( JSON_PATH_TIME , times ) ;
2020-01-25 12:22:36 +01:00
}
2020-01-27 12:06:05 +01:00
2020-04-13 23:56:25 +02:00
pub trait ListView < T : fmt ::Display + Clone > {
// get properties of implementations
fn selection_pointer ( & mut self ) -> & mut usize ;
fn list ( & mut self ) -> & mut Vec < T > ;
fn register ( & mut self ) -> & mut Option < T > ;
// specific input handling
2020-01-27 12:06:05 +01:00
fn backspace ( & mut self ) ;
fn append_to_current ( & mut self , chr : char ) ;
fn normal_mode ( & mut self ) ;
2020-04-13 23:56:25 +02:00
fn toggle_current ( & mut self ) ;
// selection manipulation
fn selection_up ( & mut self ) {
* self . selection_pointer ( ) = self . selection_pointer ( ) . saturating_sub ( 1 ) ;
}
fn selection_down ( & mut self ) {
* self . selection_pointer ( ) =
( * self . selection_pointer ( ) + 1 ) . min ( self . list ( ) . len ( ) . saturating_sub ( 1 ) ) ;
}
// adding/removing elements
fn insert ( & mut self , item : T , position : Option < usize > ) {
let pos = position . unwrap_or ( * self . selection_pointer ( ) ) ;
if pos = = self . list ( ) . len ( ) . saturating_sub ( 1 ) {
self . list ( ) . push ( item ) ;
* self . selection_pointer ( ) = self . list ( ) . len ( ) - 1 ;
} else {
self . list ( ) . insert ( pos + 1 , item ) ;
* self . selection_pointer ( ) = pos + 1 ;
}
}
fn remove_current ( & mut self ) {
if self . list ( ) . is_empty ( ) {
return ;
}
let index = * self . selection_pointer ( ) ;
* self . selection_pointer ( ) = index . min ( self . list ( ) . len ( ) . saturating_sub ( 2 ) ) ;
* self . register ( ) = self . list ( ) . remove ( index ) . into ( ) ;
}
fn paste ( & mut self ) {
let register = self . register ( ) . clone ( ) ;
match register {
Some ( item ) = > self . insert ( item , None ) ,
None = > ( ) ,
}
}
// printing
fn printable ( & mut self ) -> Vec < String > {
self . list ( ) . iter ( ) . map ( T ::to_string ) . collect ( )
}
2020-01-27 12:06:05 +01:00
}