I realized having to host this potentially indefinitely might not be the best idea, so I am going to shut down this gitea instance eventually.
You’ll have time, at least until the end of 2022, probably longer, but please just get all your stuff somewhere safe in case we ever disappear.
If any of your build scripts rely on my (kageru’s) projects hosted here, check my Github or IEW on Github for encoding projects. If you can’t find what you’re looking there, tell me to migrate it.

Compare commits

...

9 Commits

Author SHA1 Message Date
FichteFoll 84a8a26794 Automatically sort time table when adjusting times
12 months ago
FichteFoll 76e764b706 Round timestamps to whole minutes on creation
12 months ago
FichteFoll 2e35ff377b Don't needlessly display a minimum of 1 minutes
12 months ago
FichteFoll eed6aa1ac7 Don't add to pause group if it is the last item
12 months ago
FichteFoll 00647c4ddb Open with the last timesheet item selected
12 months ago
FichteFoll a7696f34cc Add key bindings to go to top or bottom of the list
12 months ago
FichteFoll 82df932a99 Ignore user files
1 year ago
FichteFoll 8974332651 Render summary vertically
1 year ago
FichteFoll 93857d0675 Don't render todo list and prevent focusing it
1 year ago

1
.gitignore vendored

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

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

@ -12,6 +12,10 @@ pub trait ListView<T: fmt::Display + Clone> {
fn normal_mode(&mut self);
// selection manipulation
fn selection_first(&mut self) {
*self.selection_pointer() = 0;
}
fn selection_up(&mut self) {
*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));
}
fn selection_last(&mut self) {
*self.selection_pointer() = 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());

@ -2,7 +2,7 @@ use super::listview::ListView;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
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};
pub struct TimeSheet {
@ -13,12 +13,13 @@ pub struct TimeSheet {
const MAIN_PAUSE_TEXT: &str = "pause";
const PAUSE_TEXTS: [&str; 4] = [MAIN_PAUSE_TEXT, "lunch", "mittag", "break"];
const END_TEXT: &str = "end";
lazy_static! {
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 {
text: String,
time: Time,
@ -28,11 +29,16 @@ impl TimePoint {
pub fn new(text: &str) -> Self {
Self {
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 {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}] {}", self.time.format("%H:%M"), self.text)
@ -72,9 +78,11 @@ fn effective_text(s: String) -> String {
impl TimeSheet {
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 {
times: read_times(path).unwrap_or_else(|| vec![TimePoint::new("start")]),
selected: 0,
times,
selected,
register: None,
}
}
@ -90,17 +98,21 @@ impl TimeSheet {
pub fn shift_current(&mut self, minutes: i64) {
let time = &mut self.times[self.selected].time;
*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 {
&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
.iter()
.chain(iter::once(&TimePoint::new("end")))
.chain(TimeSheet::maybe_end_time(last_time).iter())
.tuple_windows()
.map(|(prev, next)| (prev.text.clone(), next.time - prev.time))
// Fold into a map to group by description.
@ -111,26 +123,45 @@ impl TimeSheet {
.or_insert_with(Duration::zero) += duration;
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 {
self.grouped_times()
.into_iter()
.filter(|(text, _)| text != MAIN_PAUSE_TEXT)
.map(|(text, duration)| format!("{}: {}", text, format_duration(&duration)))
.join(" | ")
.join("\n")
}
pub fn sum_as_str(&self) -> String {
let total = self
.grouped_times()
let total = self.grouped_times()
.into_iter()
.filter(|(text, _)| text != MAIN_PAUSE_TEXT)
.fold(Duration::zero(), |total, (_, d)| total + d);
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 {
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 {

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

Loading…
Cancel
Save