Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
FichteFoll | 84a8a26794 | ||
FichteFoll | 76e764b706 | ||
FichteFoll | 2e35ff377b | ||
FichteFoll | eed6aa1ac7 | ||
FichteFoll | 00647c4ddb | ||
FichteFoll | a7696f34cc | ||
FichteFoll | 82df932a99 | ||
FichteFoll | 8974332651 | ||
FichteFoll | 93857d0675 |
|
@ -1,2 +1,3 @@
|
||||||
/target
|
/target
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
|
tracc_*.json
|
||||||
|
|
|
@ -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(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
20
src/tracc.rs
20
src/tracc.rs
|
@ -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())
|
||||||
|
|
Loading…
Reference in New Issue
Block a user