Created
April 4, 2017 14:07
-
-
Save barafael/775bcfb0d83d2a79ba8559543166f2b4 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#[macro_use] | |
extern crate clap; | |
#[macro_use] | |
extern crate serde_derive; | |
mod timesheet; | |
fn main() { | |
/* Handle command line arguments with clap */ | |
let arguments = clap_app!(trk => | |
(version: "0.1") | |
(author: "Rafael B. <mediumendian@gmail.com>") | |
(about: "Create timesheets from git history and meta info") | |
(@arg CONFIG: -c --config +takes_value "[UNUSED] Sets a custom config file") | |
(@arg debug: -d ... "[UNUSED] Sets the level of debugging information") | |
(@subcommand init => | |
(about: "Initialise trk in this directory") | |
(version: "0.1") | |
(author: "Rafael B. <mediumendian@gmail.com>") | |
(@arg name: "User name. Default is git user name if set, empty otherwise.") | |
) | |
(@subcommand begin => | |
(about: "Begin session") | |
(version: "0.1") | |
(author: "Rafael B. <mediumendian@gmail.com>") | |
) | |
(@subcommand end => | |
(about: "End session") | |
(version: "0.1") | |
(author: "Rafael B. <mediumendian@gmail.com>") | |
) | |
(@subcommand pause => | |
(about: "Pause current session") | |
(version: "0.1") | |
(author: "mediumendian@gmail.com") | |
) | |
(@subcommand metapause => | |
(about: "Pause current session and give a reason") | |
(version: "0.1") | |
(author: "mediumendian@gmail.com") | |
(@arg reason: +required "Meta information about pause") | |
) | |
(@subcommand proceed => | |
(about: "Proceed with currently paused session") | |
(version: "0.1") | |
(author: "mediumendian@gmail.com") | |
) | |
(@subcommand meta => | |
(about: "Proceed with currently paused session") | |
(version: "0.1") | |
(author: "mediumendian@gmail.com") | |
(@arg text: +required "Meta information about work") | |
) | |
(@subcommand commit => | |
(about: "add a commit to the event list") | |
(version: "0.1") | |
(author: "mediumendian@gmail.com") | |
(@arg hash: +required "Commit hash id") | |
) | |
(@subcommand branch => | |
(about: "add a topic to the event list") | |
(version: "0.1") | |
(author: "mediumendian@gmail.com") | |
(@arg name: +required "New branch name") | |
) | |
(@subcommand status => | |
(about: "prints the current WIP for session or entire sheet (eventually as json)") | |
(version: "0.1") | |
(author: "mediumendian@gmail.com") | |
(@arg which: +required "session or sheet") | |
) | |
(@subcommand clear => | |
(about: "temporary: clears the deserialized file") | |
(version: "0.1") | |
(author: "mediumendian@gmail.com") | |
) | |
) | |
.get_matches(); | |
/* Gets a value for config if supplied by user, or defaults to "default.conf" */ | |
// let config = matches.value_of("config").unwrap_or("default.conf"); | |
// println!("[UNUSED] Value for config: {}", config); | |
let sheet_opt: Option<timesheet::Timesheet> = timesheet::Timesheet::load_from_file(); | |
/* Special case for init because t_sheet can and should be None before initialisation */ | |
if let Some(command) = arguments.subcommand_matches("init") { | |
match sheet_opt { | |
Some(..) => println!("Already initialised!"), | |
None => { | |
match timesheet::Timesheet::init(command.value_of("name")) { | |
Some(..) => println!("Init successful."), | |
None => println!("Could not initialize."), | |
} | |
} | |
} | |
return; | |
} | |
/* Unwrap the timesheet and continue only if sessions file exists */ | |
let mut sheet = match sheet_opt { | |
Some(file) => file, | |
None => { | |
println!("No sessions file! You might have to init first."); | |
return; | |
} | |
}; | |
match arguments.subcommand() { | |
("begin", Some(..)) => { | |
sheet.new_session(); | |
} | |
("end", Some(..)) => { | |
sheet.end_session(); | |
} | |
("pause", Some(..)) => { | |
sheet.pause(None); | |
} | |
("metapause", Some(arg)) => { | |
let reason = arg.value_of("reason").unwrap(); | |
sheet.pause(Some(reason.to_string())); | |
} | |
("proceed", Some(..)) => { | |
sheet.proceed(); | |
} | |
("meta", Some(arg)) => { | |
let metatext = arg.value_of("text").unwrap(); | |
sheet.push_meta(metatext.to_string()); | |
} | |
("commit", Some(arg)) => { | |
let commit_hash = arg.value_of("hash").unwrap(); | |
let hash_parsed = u64::from_str_radix(commit_hash, 16).unwrap(); | |
sheet.push_commit(hash_parsed); | |
} | |
("branch", Some(arg)) => { | |
let branch_name = arg.value_of("name").unwrap(); | |
sheet.push_branch(branch_name.to_string()); | |
} | |
("status", Some(which)) => { | |
match which.value_of("which") { | |
Some("session") => println!("{:?}", sheet.last_session_status()), | |
Some("sheet") => println!("{:?}", sheet.timesheet_status()), | |
Some(text) => println!("What do you mean by {}?", text), | |
None => {} | |
} | |
} | |
("clear", Some(..)) => { | |
println!("Clearing sessions!"); | |
timesheet::Timesheet::clear_sessions(); | |
} | |
_ => unreachable!(), | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
extern crate time; | |
extern crate serde_json; | |
use std::io::prelude::*; | |
use std::time::{SystemTime, UNIX_EPOCH}; | |
use std::fs; | |
use std::path::Path; | |
use std::error::Error; | |
use std::fs::OpenOptions; | |
use std::process::Command; | |
#[derive(Serialize, Deserialize, Debug)] | |
pub enum Event { | |
Pause { time: u64 }, | |
MetaPause { time: u64, reason: String }, | |
Proceed { time: u64 }, | |
Meta { time: u64, text: String }, | |
Commit { time: u64, hash: u64 }, | |
Branch { time: u64, name: String }, | |
} | |
#[derive(Serialize, Deserialize, Debug)] | |
struct Session { | |
start: u64, | |
end: u64, | |
events: Vec<Event>, | |
} | |
impl Session { | |
fn new() -> Session { | |
let seconds = get_seconds(); | |
Session { | |
start: seconds, | |
end: seconds - 1, | |
events: Vec::<Event>::new(), | |
} | |
} | |
fn in_progress(&self) -> bool { | |
self.start == (self.end + 1) | |
} | |
fn finalize(&mut self) { | |
self.end = get_seconds(); | |
} | |
fn status(&self) -> String { | |
format!("{:?}", self) | |
} | |
fn is_paused(&self) -> bool { | |
match self.events.len() { | |
0 => false, | |
n => { | |
match self.events[n - 1] { | |
Event::Pause { .. } | | |
Event::MetaPause { .. } => true, | |
_ => false, | |
} | |
} | |
} | |
} | |
pub fn get_last_event(&mut self) -> Option<&mut Event> { | |
match self.events.len() { | |
0 => None, | |
n => Some(&mut self.events[n - 1]), | |
} | |
} | |
fn push_event(&mut self, event: Event) -> bool { | |
/* Cannot push if session is already finalized! */ | |
if !self.in_progress() { | |
println!("Already finalized, cannot push event"); | |
return false; | |
} | |
/* TODO: add logic */ | |
match event { | |
Event::Pause { .. } | | |
Event::MetaPause { .. } => { | |
if !self.is_paused() { | |
self.events.push(event); | |
true | |
} else { | |
println!("Already paused!"); | |
false | |
} | |
} | |
Event::Proceed { .. } => { | |
if self.is_paused() { | |
self.events.push(event); | |
true | |
} else { | |
println!("Currently not paused!"); | |
false | |
} | |
} | |
Event::Meta { time: metatime, text } => { | |
if self.is_paused() { | |
/* morph last pause into a MetaPause */ | |
let pause = self.events.pop().unwrap(); | |
match pause { | |
Event::Pause {time: pausetime} => { | |
self.push_event(Event::MetaPause { time: pausetime, reason: text}) | |
} | |
Event::MetaPause { time: mp_time, reason } => { | |
/* Concat? */ | |
self.push_event(Event::MetaPause { time: mp_time, reason: text}) | |
} | |
_ => unreachable!(), | |
}; | |
} else { | |
self.events.push(event); | |
}; | |
true | |
} | |
Event::Commit { .. } | | |
Event::Branch { .. } => { | |
if self.is_paused() { | |
let now = get_seconds(); | |
self.push_event(Event::Proceed { time: now }); | |
} | |
self.events.push(event); | |
true | |
} | |
} | |
} | |
} | |
#[derive(Serialize, Deserialize, Debug)] | |
pub struct Timesheet { | |
start: u64, | |
end: u64, | |
user: String, | |
sessions: Vec<Session>, | |
} | |
impl Timesheet { | |
/** Initializes the .trk/sessions.trk file which holds | |
* the serialized timesheet | |
* Returns Some(newTimesheet) if operation succeeded */ | |
pub fn init(author_name: Option<&str>) -> Option<Timesheet> { | |
/* Check if file already exists (no init permitted) */ | |
if Timesheet::is_init() { | |
None | |
} else { | |
/* File does not exist, initialize */ | |
let git_author_name = &git_author().unwrap_or("".to_string()); | |
let author_name = match author_name { | |
Some(name) => name, | |
None => git_author_name, | |
}; | |
let sheet = Timesheet { | |
start: get_seconds(), | |
end: get_seconds() - 1, | |
user: author_name.to_string(), | |
sessions: Vec::<Session>::new(), | |
}; | |
if sheet.save_to_file() { | |
Some(sheet) | |
} else { | |
None | |
} | |
} | |
} | |
fn is_init() -> bool { | |
if Path::new("./.trk/sessions.trk").exists() { | |
match Timesheet::load_from_file() { | |
Some(..) => true, | |
/* else, loading failed */ | |
None => false, | |
} | |
} else { | |
/* File doesn't even exist */ | |
false | |
} | |
} | |
fn get_last_session(&mut self) -> Option<&mut Session> { | |
match self.sessions.len() { | |
0 => None, | |
n => Some(&mut self.sessions[n - 1]), | |
} | |
} | |
pub fn new_session(&mut self) -> bool { | |
let nsessions = self.sessions.len(); | |
let pushed = match nsessions { | |
0 => true, | |
_ => { | |
if !self.sessions[nsessions - 1].in_progress() { | |
true | |
} else { | |
println!("Last session is still running!"); | |
false | |
} | |
} | |
}; | |
if pushed { | |
self.sessions.push(Session::new()); | |
self.save_to_file(); | |
} | |
pushed | |
} | |
pub fn end_session(&mut self) { | |
match self.get_last_session() { | |
Some(session) => session.finalize(), | |
None => println!("No session to finalize!"), | |
} | |
self.save_to_file(); | |
} | |
pub fn pause(&mut self, metatext: Option<String>) { | |
match self.get_last_session() { | |
Some(session) => { | |
let now = get_seconds(); | |
match metatext { | |
Some(reason) => { | |
session.push_event(Event::MetaPause { | |
time: now, | |
reason: reason.to_string(), | |
}); | |
} | |
None => { | |
session.push_event(Event::Pause { time: now }); | |
} | |
} | |
} | |
None => println!("No session to pause!"), | |
} | |
self.save_to_file(); | |
} | |
pub fn proceed(&mut self) { | |
match self.get_last_session() { | |
Some(session) => { | |
let now = get_seconds(); | |
session.push_event(Event::Proceed { time: now }); | |
} | |
None => println!("No session to pause!"), | |
} | |
self.save_to_file(); | |
} | |
pub fn push_meta(&mut self, metatext: String) { | |
match self.get_last_session() { | |
Some(session) => { | |
let now = get_seconds(); | |
session.push_event(Event::Meta { | |
time: now, | |
text: metatext, | |
}); | |
} | |
None => println!("No session to add meta to!"), | |
} | |
self.save_to_file(); | |
} | |
pub fn push_commit(&mut self, hash: u64) { | |
match self.get_last_session() { | |
Some(session) => { | |
let now = get_seconds(); | |
session.push_event(Event::Commit { | |
time: now, | |
hash: hash, | |
}); | |
} | |
None => println!("No session to add commit to!"), | |
} | |
self.save_to_file(); | |
} | |
pub fn push_branch(&mut self, name: String) { | |
match self.get_last_session() { | |
Some(session) => { | |
let now = get_seconds(); | |
session.push_event(Event::Branch { | |
time: now, | |
name: name, | |
}); | |
} | |
None => println!("No session to change branch in!"), | |
} | |
self.save_to_file(); | |
} | |
fn save_to_file(&self) -> bool { | |
/* TODO: avoid time-of-check-to-time-of-use race risk */ | |
/* TODO: make all commands run regardless of where trk is executed | |
* (and not just in root which is assumed here */ | |
let path = Path::new("./.trk/sessions.trk"); | |
let file = OpenOptions::new() | |
.write(true) | |
.truncate(true) | |
.create(true) | |
.open(&path); | |
match file { | |
Ok(mut file) => { | |
/* Convert the sheet to a JSON string. */ | |
let serialized = | |
serde_json::to_string(&self).expect("Could not write serialized time sheet!"); | |
file.write_all(serialized.as_bytes()).unwrap(); | |
/* save was successful */ | |
true | |
} | |
Err(why) => { | |
println!("{}", why.description()); | |
false | |
} | |
} | |
} | |
/** Return a Some(Timesheet) struct if a sessions.trk file is present and valid | |
* in the .trk directory, and None otherwise. | |
* TODO: improve error handling | |
* */ | |
pub fn load_from_file() -> Option<Timesheet> { | |
let path = Path::new("./.trk/sessions.trk"); | |
let file = OpenOptions::new().read(true).open(&path); | |
match file { | |
Ok(mut file) => { | |
let mut serialized = String::new(); | |
match file.read_to_string(&mut serialized) { | |
Ok(..) => serde_json::from_str(&serialized).unwrap_or(None), | |
Err(..) => { | |
println!("Reading the string failed!"); | |
None | |
} | |
} | |
} | |
Err(..) => { | |
// println!("{}", why.description()); | |
None | |
} | |
} | |
} | |
pub fn clear_sessions() { | |
/* Try to get name */ | |
let sheet = Timesheet::load_from_file(); | |
let name: Option<String> = sheet.map(|s| s.user.clone()); | |
let path = Path::new("./.trk/sessions.trk"); | |
if path.exists() { | |
match fs::remove_file(&path) { | |
Ok(..) => {} | |
Err(why) => println!("Could not remove sessions file: {}", why.description()), | |
} | |
} | |
match name { | |
Some(name) => { | |
/* Will overwrite file */ | |
Timesheet::init(Some(&name)); | |
} | |
None => { | |
Timesheet::init(None); | |
} | |
} | |
} | |
pub fn timesheet_status(&self) -> String { | |
format!("{:?}", self) | |
} | |
pub fn last_session_status(&self) -> Option<String> { | |
let nsessions = self.sessions.len(); | |
match nsessions { | |
0 => None, | |
n => Some(self.sessions[n - 1].status()), | |
} | |
} | |
} | |
fn get_seconds() -> u64 { | |
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() | |
} | |
fn git_author() -> Option<String> { | |
if let Ok(output) = Command::new("git").arg("config").arg("user.name").output() { | |
if output.status.success() { | |
let output = String::from_utf8_lossy(&output.stdout); | |
/* remove trailing newline character */ | |
let mut output = output.to_string(); | |
output.pop().expect("Empty name in git config!?!"); | |
Some(output) | |
} else { | |
let output = String::from_utf8_lossy(&output.stderr); | |
println!("git config user.name failed! {}", output); | |
None | |
} | |
} else { | |
None | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment