Skip to content

Instantly share code, notes, and snippets.

@barafael
Created April 4, 2017 14:07
Show Gist options
  • Save barafael/775bcfb0d83d2a79ba8559543166f2b4 to your computer and use it in GitHub Desktop.
Save barafael/775bcfb0d83d2a79ba8559543166f2b4 to your computer and use it in GitHub Desktop.
#[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!(),
}
}
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