Compare commits

...

9 Commits

Author SHA1 Message Date
FichteFoll 84a8a26794 Automatically sort time table when adjusting times 2021-12-10 12:18:15 +01:00
FichteFoll 76e764b706 Round timestamps to whole minutes on creation
The displayed timestamps were always floored, as were calculated
difference and its sum, which meant a duration that seemed like 1 hour
was more often than not displayed as 0:59 instead of 1:00.
2021-12-10 12:09:37 +01:00
FichteFoll 2e35ff377b Don't needlessly display a minimum of 1 minutes 2021-12-10 12:07:24 +01:00
FichteFoll eed6aa1ac7 Don't add to pause group if it is the last item
Also skip the last item if it is from the future in general.
2021-12-07 18:20:13 +01:00
FichteFoll 00647c4ddb Open with the last timesheet item selected 2021-12-06 18:55:54 +01:00
FichteFoll a7696f34cc Add key bindings to go to top or bottom of the list 2021-12-06 18:53:49 +01:00
FichteFoll 82df932a99 Ignore user files 2021-09-01 15:38:06 +02:00
FichteFoll 8974332651 Render summary vertically
With the todo list gone, it makes much more sense to render the summary
vertically (and also to isolate the pause time).
2021-09-01 15:37:03 +02:00
FichteFoll 93857d0675 Don't render todo list and prevent focusing it
This is a very quick and dirty fix, but I can strip out the entire todo
functionality later.
2021-09-01 15:35:58 +02:00
5 changed files with 67 additions and 25 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
/target /target
**/*.rs.bk **/*.rs.bk
tracc_*.json

View File

@ -20,12 +20,12 @@ pub fn selectable_list<'a, C: AsRef<str>>(
pub fn layout(r: Rect) -> Vec<Rect> { pub fn layout(r: Rect) -> Vec<Rect> {
Layout::default() Layout::default()
.direction(Direction::Vertical) .direction(Direction::Horizontal)
.constraints( .constraints(
[ [
Constraint::Percentage(0),
Constraint::Percentage(60),
Constraint::Percentage(40), Constraint::Percentage(40),
Constraint::Percentage(40),
Constraint::Percentage(20),
] ]
.as_ref(), .as_ref(),
) )

View File

@ -12,6 +12,10 @@ pub trait ListView<T: fmt::Display + Clone> {
fn normal_mode(&mut self); fn normal_mode(&mut self);
// selection manipulation // selection manipulation
fn selection_first(&mut self) {
*self.selection_pointer() = 0;
}
fn selection_up(&mut self) { fn selection_up(&mut self) {
*self.selection_pointer() = self.selection_pointer().saturating_sub(1); *self.selection_pointer() = self.selection_pointer().saturating_sub(1);
} }
@ -21,6 +25,10 @@ pub trait ListView<T: fmt::Display + Clone> {
(*self.selection_pointer() + 1).min(self.list().len().saturating_sub(1)); (*self.selection_pointer() + 1).min(self.list().len().saturating_sub(1));
} }
fn selection_last(&mut self) {
*self.selection_pointer() = self.list().len().saturating_sub(1);
}
// adding/removing elements // adding/removing elements
fn insert(&mut self, item: T, position: Option<usize>) { fn insert(&mut self, item: T, position: Option<usize>) {
let pos = position.unwrap_or(*self.selection_pointer()); let pos = position.unwrap_or(*self.selection_pointer());

View File

@ -2,7 +2,7 @@ use super::listview::ListView;
use itertools::Itertools; use itertools::Itertools;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::from_reader; use serde_json::from_reader;
use std::{collections, default, fmt, fs, io, iter}; use std::{collections, default, fmt, fs, io};
use time::{Duration, OffsetDateTime, Time}; use time::{Duration, OffsetDateTime, Time};
pub struct TimeSheet { pub struct TimeSheet {
@ -13,12 +13,13 @@ pub struct TimeSheet {
const MAIN_PAUSE_TEXT: &str = "pause"; const MAIN_PAUSE_TEXT: &str = "pause";
const PAUSE_TEXTS: [&str; 4] = [MAIN_PAUSE_TEXT, "lunch", "mittag", "break"]; const PAUSE_TEXTS: [&str; 4] = [MAIN_PAUSE_TEXT, "lunch", "mittag", "break"];
const END_TEXT: &str = "end";
lazy_static! { lazy_static! {
static ref OVERRIDE_REGEX: regex::Regex = regex::Regex::new("\\[(.*)\\]").unwrap(); static ref OVERRIDE_REGEX: regex::Regex = regex::Regex::new("\\[(.*)\\]").unwrap();
} }
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct TimePoint { pub struct TimePoint {
text: String, text: String,
time: Time, time: Time,
@ -28,11 +29,16 @@ impl TimePoint {
pub fn new(text: &str) -> Self { pub fn new(text: &str) -> Self {
Self { Self {
text: String::from(text), text: String::from(text),
time: OffsetDateTime::now_local().time(), time: now(),
} }
} }
} }
fn now() -> Time {
let raw_time = OffsetDateTime::now_local().time();
Time::try_from_hms(raw_time.hour(), raw_time.minute(), 0).unwrap()
}
impl fmt::Display for TimePoint { impl fmt::Display for TimePoint {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}] {}", self.time.format("%H:%M"), self.text) write!(f, "[{}] {}", self.time.format("%H:%M"), self.text)
@ -72,9 +78,11 @@ fn effective_text(s: String) -> String {
impl TimeSheet { impl TimeSheet {
pub fn open_or_create(path: &str) -> Self { pub fn open_or_create(path: &str) -> Self {
let times = read_times(path).unwrap_or_else(|| vec![TimePoint::new("start")]);
let selected = times.len().saturating_sub(1);
Self { Self {
times: read_times(path).unwrap_or_else(|| vec![TimePoint::new("start")]), times,
selected: 0, selected,
register: None, register: None,
} }
} }
@ -90,17 +98,21 @@ impl TimeSheet {
pub fn shift_current(&mut self, minutes: i64) { pub fn shift_current(&mut self, minutes: i64) {
let time = &mut self.times[self.selected].time; let time = &mut self.times[self.selected].time;
*time += Duration::minutes(minutes); *time += Duration::minutes(minutes);
*time -= Duration::minutes(time.minute() as i64 % 5) *time -= Duration::minutes(time.minute() as i64 % 5);
let timepoint = self.times[self.selected].clone();
self.times.sort_by_key(|tp| tp.time);
self.selected = self.times.iter().position(|tp| tp == &timepoint).unwrap();
} }
fn current(&self) -> &TimePoint { fn current(&self) -> &TimePoint {
&self.times[self.selected] &self.times[self.selected]
} }
fn grouped_times(&self) -> impl Iterator<Item = (String, Duration)> { fn grouped_times(&self) -> collections::BTreeMap<String, Duration> {
let last_time = self.times.last();
self.times self.times
.iter() .iter()
.chain(iter::once(&TimePoint::new("end"))) .chain(TimeSheet::maybe_end_time(last_time).iter())
.tuple_windows() .tuple_windows()
.map(|(prev, next)| (prev.text.clone(), next.time - prev.time)) .map(|(prev, next)| (prev.text.clone(), next.time - prev.time))
// Fold into a map to group by description. // Fold into a map to group by description.
@ -111,26 +123,45 @@ impl TimeSheet {
.or_insert_with(Duration::zero) += duration; .or_insert_with(Duration::zero) += duration;
map map
}) })
.into_iter() }
fn maybe_end_time(last_time: Option<&TimePoint>) -> Option<TimePoint> {
match last_time {
Some(tp) if PAUSE_TEXTS.contains(&&tp.text[..]) => None,
Some(tp) if tp.text == END_TEXT => None,
Some(tp) if tp.time > now() => None,
_ => Some(TimePoint::new(END_TEXT)),
}
} }
pub fn time_by_tasks(&self) -> String { pub fn time_by_tasks(&self) -> String {
self.grouped_times() self.grouped_times()
.into_iter()
.filter(|(text, _)| text != MAIN_PAUSE_TEXT)
.map(|(text, duration)| format!("{}: {}", text, format_duration(&duration))) .map(|(text, duration)| format!("{}: {}", text, format_duration(&duration)))
.join(" | ") .join("\n")
} }
pub fn sum_as_str(&self) -> String { pub fn sum_as_str(&self) -> String {
let total = self let total = self.grouped_times()
.grouped_times() .into_iter()
.filter(|(text, _)| text != MAIN_PAUSE_TEXT) .filter(|(text, _)| text != MAIN_PAUSE_TEXT)
.fold(Duration::zero(), |total, (_, d)| total + d); .fold(Duration::zero(), |total, (_, d)| total + d);
format_duration(&total) format_duration(&total)
} }
pub fn pause_time(&self) -> String {
let times = self.grouped_times();
let duration = times
.get(MAIN_PAUSE_TEXT)
.map(Duration::clone)
.unwrap_or_else(Duration::zero);
format!("{}: {}", MAIN_PAUSE_TEXT, format_duration(&duration))
}
} }
fn format_duration(d: &Duration) -> String { fn format_duration(d: &Duration) -> String {
format!("{}:{:02}", d.whole_hours(), d.whole_minutes().max(1) % 60) format!("{}:{:02}", d.whole_hours(), d.whole_minutes() % 60)
} }
impl ListView<TimePoint> for TimeSheet { impl ListView<TimePoint> for TimeSheet {

View File

@ -39,7 +39,7 @@ impl Tracc {
times: TimeSheet::open_or_create(JSON_PATH_TIME), times: TimeSheet::open_or_create(JSON_PATH_TIME),
terminal, terminal,
input_mode: Mode::Normal, input_mode: Mode::Normal,
focus: Focus::Top, focus: Focus::Bottom,
} }
} }
@ -51,7 +51,7 @@ impl Tracc {
Focus::Bottom => $action(&mut self.times, $($arg,)*), Focus::Bottom => $action(&mut self.times, $($arg,)*),
} }
}; };
}; }
let mut inputs = io::stdin().keys(); let mut inputs = io::stdin().keys();
loop { loop {
@ -63,6 +63,13 @@ impl Tracc {
Key::Char('q') => break, Key::Char('q') => break,
Key::Char('j') => with_focused!(ListView::selection_down), Key::Char('j') => with_focused!(ListView::selection_down),
Key::Char('k') => with_focused!(ListView::selection_up), Key::Char('k') => with_focused!(ListView::selection_up),
Key::Char('G') => with_focused!(ListView::selection_first),
// gg
Key::Char('g') => {
if let Some(Ok(Key::Char('g'))) = inputs.next() {
with_focused!(ListView::selection_last);
}
}
Key::Char('o') => { Key::Char('o') => {
with_focused!(ListView::insert, Default::default(), None); with_focused!(ListView::insert, Default::default(), None);
self.set_mode(Mode::Insert)?; self.set_mode(Mode::Insert)?;
@ -87,12 +94,6 @@ impl Tracc {
} }
} }
Key::Char('p') => with_focused!(ListView::paste), Key::Char('p') => with_focused!(ListView::paste),
Key::Char('\t') => {
self.focus = match self.focus {
Focus::Top => Focus::Bottom,
Focus::Bottom => Focus::Top,
}
}
_ => (), _ => (),
}, },
Mode::Insert => match input { Mode::Insert => match input {
@ -124,8 +125,9 @@ impl Tracc {
fn refresh(&mut self) -> Result<(), io::Error> { fn refresh(&mut self) -> Result<(), io::Error> {
let summary_content = [Text::raw(format!( let summary_content = [Text::raw(format!(
"Sum for today: {}\n{}", "Sum for today: {}\n{}\n\n{}",
self.times.sum_as_str(), self.times.sum_as_str(),
self.times.pause_time(),
self.times.time_by_tasks() self.times.time_by_tasks()
))]; ))];
let mut summary = Paragraph::new(summary_content.iter()) let mut summary = Paragraph::new(summary_content.iter())