use std::collections::HashSet;
use std::fs;
use std::io;
use std::num::NonZeroU32;
use std::process::{Command, Stdio};
use std::result;
use xml::writer::{EmitterConfig, XmlEvent};
#[doc(inline)]
use crate::archive::Archiver;
use crate::buf_reader;
#[doc(inline)]
use crate::cli::args::DateRangeArgs;
use crate::cli::args::DayFilter;
#[doc(inline)]
use crate::cli::args::FilterArgs;
use crate::config::Config;
#[doc(inline)]
use crate::date::{Date, DateTime, Time};
use crate::day::format_dur;
#[doc(inline)]
use crate::day::Day;
use crate::emit_xml;
#[doc(inline)]
use crate::entry::{Entry, EntryKind, PROJECT_RE};
#[doc(inline)]
use crate::error::Error;
#[doc(inline)]
use crate::error::PathError;
#[doc(inline)]
use crate::logfile::Logfile;
#[doc(inline)]
use crate::stack::Stack;
#[doc(inline)]
use crate::task_line_iter::TaskLineIter;
const STYLESHEET: &str = r"
.day {
display: grid;
grid-template-columns: 25% 1fr;
grid-template-rows: 4em auto;
padding-left: 0.5em;
padding-bottom: 2em;
}
.day:nth-child(even) { background-color: #eee; }
.day + .day {
border-top: 1px solid black;
}
.tasks {
display: grid;
grid-template-columns: auto auto auto;
grid-template-rows: repeat(auto-file);
}
@media screen and (min-width:400px) {
.day { grid-template-columns: auto auto; }
.tasks { grid-template-columns: auto; }
}
@media screen and (min-width:1024px) {
.day { grid-template-columns: 40% 1fr; }
.tasks { grid-template-columns: auto auto; }
}
@media screen and (min-width:1250px) {
.day { grid-template-columns: 30% 1fr; }
.tasks { grid-template-columns: auto auto auto; }
}
@media screen and (min-width:1400px) {
.day { grid-template-columns: 25% 1fr; }
.tasks { grid-template-columns: auto auto auto auto; }
}
.day .project {
grid-column: 1;
grid-row: 1 / span 2;
}
.day .tasks {
grid-column: 2;
grid-row: 2;
}
.project h2 { margin-bottom: 2em; }
.piechart {
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: auto;
}
.piechart .pie { grid-column: 1; grid-row: 1; }
.piechart table { grid-column: 2; grid-row: 1; }
.hours { grid-column: 1 / span 2; grid-row: 2; }
.hours h3 { margin-left: 5em; margin-bottom: 0.5ex; }
table.legend { margin-left: 0.75em; margin-bottom: auto; }
.legend span { margin-left: 0.25em; }
.legend td {
display: flex;
align-items: center;
}
";
fn stack(config: &Config) -> crate::Result<Stack> { Ok(Stack::new(&config.stackfile())?) }
fn logfile(config: &Config) -> crate::Result<Logfile> { Ok(Logfile::new(&config.logfile())?) }
pub fn initialize(config: &Config, dir: Option<&str>) -> result::Result<(), PathError> {
let mut config = config.clone();
let dir = dir.unwrap_or(config.dir());
fs::create_dir_all(dir)
.map_err(|e| PathError::CantCreatePath(dir.to_string(), e.to_string()))?;
let candir = fs::canonicalize(dir)
.map_err(|e| PathError::InvalidPath(dir.to_string(), e.to_string()))?;
let candir = candir.to_str().ok_or_else(|| {
PathError::InvalidPath(dir.to_string(), String::from("Directory name not valid"))
})?;
config.set_dir(candir);
config.create()?;
Ok(())
}
pub fn start_task(config: &Config, args: &[String]) -> crate::Result<()> {
let logfile = logfile(config)?;
Ok(logfile.add_task(&args.join(" "))?)
}
pub fn stop_task(config: &Config) -> crate::Result<()> {
let logfile = logfile(config)?;
Ok(logfile.add_task("stop")?)
}
pub fn add_comment(config: &Config, args: &[String]) -> crate::Result<()> {
let logfile = logfile(config)?;
Ok(logfile.add_comment(&args.join(" "))?)
}
pub fn add_event(config: &Config, args: &[String]) -> crate::Result<()> {
let logfile = logfile(config)?;
Ok(logfile.add_event(&args.join(" "))?)
}
pub fn push_task(config: &Config, args: &[String]) -> crate::Result<()> {
let logfile = logfile(config)?;
let entry = logfile.last_entry()?;
if entry.entry_text().is_empty() {
return Ok(());
}
let stack = stack(config)?;
stack.push(entry.entry_text())?;
logfile.add_task(&args.join(" ")).map_err(Into::into)
}
pub fn resume_task(config: &Config) -> crate::Result<()> {
let stack = stack(config)?;
if let Some(task) = stack.pop() {
logfile(config)?.add_task(&task)?;
}
Ok(())
}
pub fn pause_task(config: &Config) -> crate::Result<()> { push_task(config, &["stop".to_string()]) }
pub fn discard_last_entry(config: &Config) -> crate::Result<()> {
logfile(config)?.discard_line().map_err(Into::into)
}
#[rustfmt::skip]
pub fn reset_last_entry(config: &Config) -> crate::Result<()> {
logfile(config)?.reset_last_entry()
}
pub fn rewrite_last_entry(config: &Config, args: &[String]) -> crate::Result<()> {
logfile(config)?.rewrite_last_entry(&args.join(" "))
}
pub fn retime_last_entry(config: &Config, time: Time) -> crate::Result<()> {
logfile(config)?.retime_last_entry(time)
}
pub fn rewind_last_entry(config: &Config, minutes: NonZeroU32) -> crate::Result<()> {
logfile(config)?.rewind_last_entry(minutes)
}
pub fn ignore_last_entry(config: &Config) -> crate::Result<()> {
logfile(config)?.ignore_last_entry()
}
pub fn list_entries(config: &Config, date: Option<&str>, all: bool) -> crate::Result<()> {
use std::io::BufRead;
let file = logfile(config)?.open()?;
let start = Date::parse(date.unwrap_or("today"))?;
let end = &start.succ().to_string();
let start = start.to_string();
if all {
for line in TaskLineIter::new(
io::BufReader::new(file).lines().map_while(Result::ok),
&start,
end
)? {
println!("{line}");
}
}
else {
for line in TaskLineIter::new(
io::BufReader::new(file).lines().map_while(Result::ok),
&start,
end
)?.filter(|e| !Entry::is_event_line(e)) {
println!("{line}");
}
}
Ok(())
}
pub fn list_projects(config: &Config) -> crate::Result<()> {
use std::io::BufRead;
let mut projects: HashSet<String> = HashSet::new();
let file = logfile(config)?.open()?;
io::BufReader::new(file)
.lines()
.map_while(Result::ok)
.for_each(|ln| {
if let Some(proj) = PROJECT_RE.captures(&ln) {
if let Some(p) = proj.get(1) {
projects.insert(p.as_str().to_string());
}
}
});
let mut names: Vec<String> = projects.iter().map(ToString::to_string).collect();
names.as_mut_slice().sort();
for p in &names {
println!("{p}");
}
Ok(())
}
pub fn list_stack(config: &Config) -> crate::Result<()> {
let stackfile = stack(config)?;
if !stackfile.exists() { return Ok(()); }
stackfile.process_down_stack(|i, ln| println!("{}) {ln}", i + 1))
}
pub fn edit(config: &Config) -> crate::Result<()> {
Command::new(config.editor())
.arg(config.logfile())
.status()
.map_err(|e| Error::EditorFailed(config.editor().to_string(), e.to_string()))?;
Ok(())
}
fn start_day(start: &str, prev: Option<&Entry>) -> crate::Result<Day> {
let mut day = Day::new(start)?;
if let Some(p) = prev.as_ref() {
day.start_day(p)?;
}
Ok(day)
}
fn report<F>(config: &Config, filter: &dyn DayFilter, mut f: F) -> crate::Result<()>
where
F: FnMut(&Day) -> crate::Result<()>
{
let start = filter.start();
let file = logfile(config)?.open()?;
let mut prev: Option<Entry> = TaskLineIter::new(buf_reader(file), &start, &filter.end())?
.last_line_before()
.and_then(|l| Entry::from_line(&l).ok())
.map(|e| e.to_day_end());
let mut day = start_day(&start, prev.as_ref())?;
let file = logfile(config)?.open()?;
for line in TaskLineIter::new(buf_reader(file), &start, &filter.end())? {
let entry = Entry::from_line(&line)?;
let stamp = entry.stamp();
if day.date_stamp() != stamp {
if !day.is_complete() {
if let Some(prev_entry) = prev {
let day_end = prev_entry.to_day_end();
day.add_entry(day_end.clone())?;
prev = Some(day_end);
}
}
if let Some(filtered) = filter.filter_day(day) {
f(&filtered)?;
}
day = start_day(&stamp, prev.as_ref())?;
}
day.add_entry(entry.clone())?;
prev = Some(entry);
}
day.finish()?;
if let Some(filtered) = filter.filter_day(day) {
f(&filtered)?;
}
Ok(())
}
fn report_file(filename: &str) -> crate::Result<fs::File> {
Ok(fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(filename)
.map_err(|e| PathError::FileAccess(filename.to_string(), e.to_string()))?)
}
fn launch_chart(config: &Config, filename: &str) -> crate::Result<()> {
Command::new(config.browser())
.arg(filename)
.stderr(Stdio::null()) .status()
.map_err(|e| Error::EditorFailed(config.browser().to_string(), e.to_string()))?;
Ok(())
}
pub fn chart_daily(config: &Config, dates: &[String]) -> crate::Result<()> {
let filename = config.reportfile();
let mut file = report_file(&filename)?;
let mut w = EmitterConfig::new()
.perform_indent(true)
.write_document_declaration(false)
.create_writer(&mut file);
emit_xml!(&mut w, html => {
emit_xml!(w, head => {
emit_xml!(w, title; "Daily Timelog Report")?;
emit_xml!(w, style, type: "text/css"; STYLESHEET)
})?;
emit_xml!(w, body => {
report(config, &DateRangeArgs::new(dates)?, |day|
day.daily_chart().write(&mut w)
)
})
})?;
launch_chart(config, &filename)
}
pub fn report_daily(config: &Config, dates: &[String], projs: &[String]) -> crate::Result<()> {
report(config, &FilterArgs::new(dates, projs)?, |day| {
print!("{}", day.detail_report());
Ok(())
})
}
pub fn report_summary(config: &Config, dates: &[String], projs: &[String]) -> crate::Result<()> {
report(config, &FilterArgs::new(dates, projs)?, |day| {
print!("{}", day.summary_report());
Ok(())
})
}
pub fn report_hours(config: &Config, dates: &[String], projs: &[String]) -> crate::Result<()> {
report(config, &FilterArgs::new(dates, projs)?, |day| {
print!("{}", day.hours_report());
Ok(())
})
}
pub fn report_events(
config: &Config, dates: &[String], projs: &[String], compact: bool
) -> crate::Result<()> {
report(config, &FilterArgs::new(dates, projs)?, |day| {
print!("{}", day.event_report(compact));
Ok(())
})
}
pub fn report_intervals(config: &Config, dates: &[String], projs: &[String]) -> crate::Result<()> {
let mut events: Vec<Entry> = vec![];
let filter = FilterArgs::new(dates, projs)?;
report(config, &filter, |day| {
events.append(&mut day.events().cloned().collect());
Ok(())
})?;
if events.is_empty() { return Ok(()); }
events.push(Entry::new_marked("now", DateTime::now(), EntryKind::Event));
for [first, second] in events.array_windows() {
#[rustfmt::skip]
println!("{} {} : {}",
first.timestamp(),
first.entry_text(),
format_dur(&(second.date_time() - first.date_time())?)
);
}
Ok(())
}
pub fn current_task(config: &Config) -> crate::Result<()> {
let entry = logfile(config)?.last_entry()?;
if entry.is_stop() {
println!("Not in entry.");
}
else {
println!("{}", &entry);
let dur = (DateTime::now() - entry.date_time())?;
println!("Duration: {}", format_dur(&dur));
}
Ok(())
}
pub fn archive_year(config: &Config) -> crate::Result<()> {
match Archiver::new(config).archive()? {
None => println!("Nothing to archive"),
Some(year) => println!("{year} archived")
}
Ok(())
}
pub fn list_aliases(config: &Config) {
let mut aliases: Vec<&str> = config.alias_names().map(String::as_str).collect();
aliases.as_mut_slice().sort();
let maxlen: usize = aliases.iter().map(|a| a.len()).max().unwrap_or_default();
println!("Aliases:");
for alias in aliases {
if let Some(value) = config.alias(alias) {
println!(" {1:0$} : {2}", &maxlen, &alias, value);
}
}
}
pub fn check_logfile(config: &Config) -> crate::Result<()> {
let problems = logfile(config)?.problems();
if problems.is_empty() {
println!("No problems found");
}
else {
for p in problems {
println!("{p}");
}
}
Ok(())
}
pub fn swap_entry(config: &Config) -> crate::Result<()> {
let stack = stack(config)?;
let logfile = logfile(config)?;
if let Some(curr_task) = logfile.last_line() {
if let Some(task) = stack.pop() {
logfile.add_task(&task)?;
}
let entry = Entry::from_line(&curr_task)?;
if !entry.is_stop() {
stack.push(entry.entry_text())?;
}
}
Ok(())
}
pub fn stack_keep(config: &Config, num: NonZeroU32) -> crate::Result<()> {
let stack = stack(config)?;
stack.keep(num)
}
pub fn stack_clear(config: &Config) -> crate::Result<()> {
let stack = stack(config)?;
stack.clear().map_err(Into::into)
}
pub fn stack_drop(config: &Config, num: NonZeroU32) -> crate::Result<()> {
let stack = stack(config)?;
stack.drop(num)
}
pub fn stack_top(config: &Config) -> crate::Result<()> {
let stack = stack(config)?;
println!("{}", stack.top()?);
Ok(())
}