timelog/cli/cmd.rs
1//! Implementations of the commands run by the CLI
2
3use std::collections::HashSet;
4use std::fs;
5use std::io;
6use std::num::NonZeroU32;
7use std::process::{Command, Stdio};
8use std::result;
9
10use xml::writer::{EmitterConfig, XmlEvent};
11
12#[doc(inline)]
13use crate::archive::Archiver;
14use crate::buf_reader;
15#[doc(inline)]
16use crate::cli::args::DateRangeArgs;
17use crate::cli::args::DayFilter;
18#[doc(inline)]
19use crate::cli::args::FilterArgs;
20use crate::config::Config;
21#[doc(inline)]
22use crate::date::{Date, DateTime, Time};
23use crate::day::format_dur;
24#[doc(inline)]
25use crate::day::Day;
26use crate::emit_xml;
27#[doc(inline)]
28use crate::entry::{Entry, EntryKind, PROJECT_RE};
29#[doc(inline)]
30use crate::error::Error;
31#[doc(inline)]
32use crate::error::PathError;
33#[doc(inline)]
34use crate::logfile::Logfile;
35#[doc(inline)]
36use crate::stack::Stack;
37#[doc(inline)]
38use crate::task_line_iter::TaskLineIter;
39
40/// The style definition for the chart HTML.
41const STYLESHEET: &str = r"
42 .day {
43 display: grid;
44 grid-template-columns: 25% 1fr;
45 grid-template-rows: 4em auto;
46 padding-left: 0.5em;
47 padding-bottom: 2em;
48 }
49 .day:nth-child(even) { background-color: #eee; }
50 .day + .day {
51 border-top: 1px solid black;
52 }
53 .tasks {
54 display: grid;
55 grid-template-columns: auto auto auto;
56 grid-template-rows: repeat(auto-file);
57 }
58 @media screen and (min-width:400px) {
59 .day { grid-template-columns: auto auto; }
60 .tasks { grid-template-columns: auto; }
61 }
62 @media screen and (min-width:1024px) {
63 .day { grid-template-columns: 40% 1fr; }
64 .tasks { grid-template-columns: auto auto; }
65 }
66 @media screen and (min-width:1250px) {
67 .day { grid-template-columns: 30% 1fr; }
68 .tasks { grid-template-columns: auto auto auto; }
69 }
70 @media screen and (min-width:1400px) {
71 .day { grid-template-columns: 25% 1fr; }
72 .tasks { grid-template-columns: auto auto auto auto; }
73 }
74 .day .project {
75 grid-column: 1;
76 grid-row: 1 / span 2;
77 }
78 .day .tasks {
79 grid-column: 2;
80 grid-row: 2;
81 }
82 .project h2 { margin-bottom: 2em; }
83 .piechart {
84 display: grid;
85 grid-template-columns: auto 1fr;
86 grid-template-rows: auto;
87 }
88 .piechart .pie { grid-column: 1; grid-row: 1; }
89 .piechart table { grid-column: 2; grid-row: 1; }
90 .hours { grid-column: 1 / span 2; grid-row: 2; }
91 .hours h3 { margin-left: 5em; margin-bottom: 0.5ex; }
92 table.legend { margin-left: 0.75em; margin-bottom: auto; }
93 .legend span { margin-left: 0.25em; }
94 .legend td {
95 display: flex;
96 align-items: center;
97 }
98";
99
100// Utility function to return the current stack file.
101//
102// # Errors
103//
104// - Return [`PathError::FilenameMissing`] if the `file` has no filename.
105// - Return [`PathError::InvalidPath`] if the path part of `file` is not a valid path.
106fn stack(config: &Config) -> crate::Result<Stack> { Ok(Stack::new(&config.stackfile())?) }
107
108// Utility function to return the current log file.
109//
110// # Errors
111//
112// - Return [`PathError::FilenameMissing`] if the `file` has no filename.
113// - Return [`PathError::InvalidPath`] if the path part of `file` is not a valid path.
114fn logfile(config: &Config) -> crate::Result<Logfile> { Ok(Logfile::new(&config.logfile())?) }
115
116/// Initialize the timelog directory supplied and create a `.timelogrc` config file.
117/// If no directory is supplied default to `~/timelog`
118///
119/// # Errors
120///
121/// - Return [`PathError::CantCreatePath`] if cannot create timelog directory
122/// - Return [`PathError::FileAccess`] if we are unable to write the configuration.
123/// - Return [`PathError::InvalidPath`] if the path is not valid
124///
125/// ## Panics
126///
127/// If the canonicalized path cannot be converted to a string.
128pub fn initialize(config: &Config, dir: Option<&str>) -> result::Result<(), PathError> {
129 let mut config = config.clone();
130 let dir = dir.unwrap_or(config.dir());
131
132 fs::create_dir_all(dir)
133 .map_err(|e| PathError::CantCreatePath(dir.to_string(), e.to_string()))?;
134
135 let candir = fs::canonicalize(dir)
136 .map_err(|e| PathError::InvalidPath(dir.to_string(), e.to_string()))?;
137 // Convert type
138 let candir = candir.to_str().ok_or_else(|| {
139 PathError::InvalidPath(dir.to_string(), String::from("Directory name not valid"))
140 })?;
141
142 config.set_dir(candir);
143 config.create()?;
144 Ok(())
145}
146
147/// Start a task.
148/// Add an entry to the logfile marked with the current date and time and the supplied task
149/// description.
150///
151/// # Errors
152///
153/// - Return [`PathError::FilenameMissing`] if the `file` has no filename.
154/// - Return [`PathError::InvalidPath`] if the path part of `file` is not a valid path.
155/// - Return [`PathError::FileAccess`] if the file cannot be opened or created.
156/// - Return [`PathError::FileWrite`] if the function fails to append to the file.
157pub fn start_task(config: &Config, args: &[String]) -> crate::Result<()> {
158 let logfile = logfile(config)?;
159 Ok(logfile.add_task(&args.join(" "))?)
160}
161
162/// Stop a task.
163/// Add the 'stop' entry to the logfile marked with the current date and time.
164///
165/// # Errors
166///
167/// - Return [`PathError::FilenameMissing`] if the log file has no filename.
168/// - Return [`PathError::InvalidPath`] if the path part of log file is not a valid path.
169/// - Return [`PathError::FileAccess`] if the log file cannot be opened or created.
170/// - Return [`PathError::FileWrite`] if the function fails to append to the log file.
171pub fn stop_task(config: &Config) -> crate::Result<()> {
172 let logfile = logfile(config)?;
173 Ok(logfile.add_task("stop")?)
174}
175
176/// Add a comment line to the logfile
177///
178/// # Errors
179///
180/// - Return [`PathError::FilenameMissing`] if the log file has no filename.
181/// - Return [`PathError::InvalidPath`] if the path part of log file is not a valid path.
182/// - Return [`PathError::FileAccess`] if the log file cannot be opened or created.
183/// - Return [`PathError::FileWrite`] if the function fails to append to the log file.
184pub fn add_comment(config: &Config, args: &[String]) -> crate::Result<()> {
185 let logfile = logfile(config)?;
186 Ok(logfile.add_comment(&args.join(" "))?)
187}
188
189/// Add a zero duration event to the logfile
190///
191/// # Errors
192///
193/// - Return [`PathError::FilenameMissing`] if the log file has no filename.
194/// - Return [`PathError::InvalidPath`] if the path part of log file is not a valid path.
195/// - Return [`PathError::FileAccess`] if the log file cannot be opened or created.
196/// - Return [`PathError::FileWrite`] if the function fails to append to the log file.
197pub fn add_event(config: &Config, args: &[String]) -> crate::Result<()> {
198 let logfile = logfile(config)?;
199 Ok(logfile.add_event(&args.join(" "))?)
200}
201
202/// Start a task and saving the current entry description to the stack.
203///
204/// Add the task entry description to the logfile marked with the current date and time.
205///
206/// # Errors
207///
208/// - Return [`PathError::FilenameMissing`] if the log file or stack file is missing.
209/// - Return [`PathError::InvalidPath`] if the path part of the log or stack file is not a valid
210/// path.
211/// - Return [`PathError::FileAccess`] if the file cannot be opened or created.
212/// - Return [`PathError::FileWrite`] if the function fails to append to the file.
213pub fn push_task(config: &Config, args: &[String]) -> crate::Result<()> {
214 let logfile = logfile(config)?;
215 let entry = logfile.last_entry()?;
216 if entry.entry_text().is_empty() {
217 return Ok(());
218 }
219
220 let stack = stack(config)?;
221 stack.push(entry.entry_text())?;
222
223 logfile.add_task(&args.join(" ")).map_err(Into::into)
224}
225
226/// Resume the previous task entry by popping it off the stack and starting that task at the
227/// current date and time.
228///
229/// # Errors
230///
231/// - Return [`PathError::FilenameMissing`] if the log file or stack file is missing.
232/// - Return [`PathError::InvalidPath`] if the path part of the log or stack file is not a valid
233/// path.
234/// - Return [`PathError::FileAccess`] if the file cannot be opened or created.
235/// - Return [`PathError::FileWrite`] if the function fails to append to the file.
236pub fn resume_task(config: &Config) -> crate::Result<()> {
237 let stack = stack(config)?;
238 if let Some(task) = stack.pop() {
239 logfile(config)?.add_task(&task)?;
240 }
241 Ok(())
242}
243
244/// Pause the current task by placing it on the stack and stopping timing.
245///
246/// # Errors
247///
248/// - Return [`PathError::FilenameMissing`] if the log file or stack file is missing.
249/// - Return [`PathError::InvalidPath`] if the path part of the log or stack file is not a valid
250/// path.
251/// - Return [`PathError::FileAccess`] if the file cannot be opened or created.
252/// - Return [`PathError::FileWrite`] if the function fails to append to the file.
253pub fn pause_task(config: &Config) -> crate::Result<()> { push_task(config, &["stop".to_string()]) }
254
255/// Discard the most recent entry in the logfile.
256///
257/// # Errors
258///
259/// - Return [`PathError::FileAccess`] if the file cannot be opened.
260pub fn discard_last_entry(config: &Config) -> crate::Result<()> {
261 logfile(config)?.discard_line().map_err(Into::into)
262}
263
264/// Reset the datestamp on the most recent entry to now.
265///
266/// # Errors
267///
268/// - Return [`PathError::FileAccess`] if the file cannot be opened.
269#[rustfmt::skip]
270pub fn reset_last_entry(config: &Config) -> crate::Result<()> {
271 logfile(config)?.reset_last_entry()
272}
273
274/// Replace the task text on the most recent entry.
275///
276/// # Errors
277///
278/// - Return [`PathError::FileAccess`] if the file cannot be opened.
279pub fn rewrite_last_entry(config: &Config, args: &[String]) -> crate::Result<()> {
280 logfile(config)?.rewrite_last_entry(&args.join(" "))
281}
282
283/// Replace the task time on the most recent entry.
284///
285/// # Errors
286///
287/// - Return [`PathError::FileAccess`] if the file cannot be opened.
288pub fn retime_last_entry(config: &Config, time: Time) -> crate::Result<()> {
289 logfile(config)?.retime_last_entry(time)
290}
291
292/// Shift the time back the number of minutes on the most recent entry.
293///
294/// # Errors
295///
296/// - Return [`PathError::FileAccess`] if the file cannot be opened.
297pub fn rewind_last_entry(config: &Config, minutes: NonZeroU32) -> crate::Result<()> {
298 logfile(config)?.rewind_last_entry(minutes)
299}
300
301/// Mark the most recent entry as ignored
302///
303/// # Errors
304///
305/// - Return [`PathError::FileAccess`] if the file cannot be opened.
306pub fn ignore_last_entry(config: &Config) -> crate::Result<()> {
307 logfile(config)?.ignore_last_entry()
308}
309
310/// List the entries for a particular date, or today if none is supplied.
311///
312/// # Errors
313///
314/// - Return [`PathError::FilenameMissing`] if the log file is missing.
315/// - Return [`PathError::InvalidPath`] if the path part of the log file is not a valid path.
316/// - Return [`PathError::FileAccess`] if the file cannot be opened.
317pub fn list_entries(config: &Config, date: Option<&str>, all: bool) -> crate::Result<()> {
318 use std::io::BufRead;
319
320 let file = logfile(config)?.open()?;
321
322 let start = Date::parse(date.unwrap_or("today"))?;
323 let end = &start.succ().to_string();
324 let start = start.to_string();
325
326 if all {
327 for line in TaskLineIter::new(
328 io::BufReader::new(file).lines().map_while(Result::ok),
329 &start,
330 end
331 )? {
332 println!("{line}");
333 }
334 }
335 else {
336 for line in TaskLineIter::new(
337 io::BufReader::new(file).lines().map_while(Result::ok),
338 &start,
339 end
340 )?.filter(|e| !Entry::is_event_line(e)) {
341 println!("{line}");
342 }
343 }
344 Ok(())
345}
346
347/// List the projects in the logfile.
348///
349/// # Errors
350///
351/// - Return [`PathError::FilenameMissing`] if the log file is missing.
352/// - Return [`PathError::InvalidPath`] if the path part of the log file is not a valid path.
353/// - Return [`PathError::FileAccess`] if the file cannot be opened.
354pub fn list_projects(config: &Config) -> crate::Result<()> {
355 use std::io::BufRead;
356
357 let mut projects: HashSet<String> = HashSet::new();
358
359 let file = logfile(config)?.open()?;
360 io::BufReader::new(file)
361 .lines()
362 .map_while(Result::ok)
363 .for_each(|ln| {
364 if let Some(proj) = PROJECT_RE.captures(&ln) {
365 // Since we've verified the captures, I expect it's safe to get the one.
366 if let Some(p) = proj.get(1) {
367 projects.insert(p.as_str().to_string());
368 }
369 }
370 });
371
372 let mut names: Vec<String> = projects.iter().map(ToString::to_string).collect();
373 names.as_mut_slice().sort();
374 for p in &names {
375 println!("{p}");
376 }
377
378 Ok(())
379}
380
381/// List the items on the stack, most recent first.
382///
383/// # Errors
384///
385/// - Return [`PathError::FilenameMissing`] if the log or stack file is missing.
386/// - Return [`PathError::InvalidPath`] if the path part of the log or stack file is not a valid
387/// path.
388/// - Return [`PathError::FileAccess`] if the file cannot be opened.
389pub fn list_stack(config: &Config) -> crate::Result<()> {
390 let stackfile = stack(config)?;
391 if !stackfile.exists() { return Ok(()); }
392
393 stackfile.process_down_stack(|i, ln| println!("{}) {ln}", i + 1))
394}
395
396/// Launch the configured editor to edit the logfile.
397///
398/// # Errors
399///
400/// - Return [`Error::EditorFailed`] if unable to run the editor.
401pub fn edit(config: &Config) -> crate::Result<()> {
402 Command::new(config.editor())
403 .arg(config.logfile())
404 .status()
405 .map_err(|e| Error::EditorFailed(config.editor().to_string(), e.to_string()))?;
406
407 Ok(())
408}
409
410// Utility function for creating a day and initializing it if a previous entry was still open.
411fn start_day(start: &str, prev: Option<&Entry>) -> crate::Result<Day> {
412 let mut day = Day::new(start)?;
413 if let Some(p) = prev.as_ref() {
414 day.start_day(p)?;
415 }
416 Ok(day)
417}
418
419// Generate a report from the entries in the logfile based on the supplied `args`.
420//
421// Uses the supplied filter object to apply appropriate filtering conditions that
422// select the entries of interest from the logfile. These entries are collected into
423// [`Day`]s which are then reported using the supplied function `f`.
424//
425// The purpose of this method is to handle all of the boring grunt-work of finding the
426// entries in question and collecting them together to generate one or more daily reports.
427//
428// # Errors
429//
430// - Return [`PathError::FilenameMissing`] if the log file is missing.
431// - Return [`PathError::InvalidPath`] if the path part of the log file is not a valid path.
432// - Return [`PathError::FileAccess`] if the file cannot be opened.
433// - Return [`Error::BadProjectFilter`] if the supplied project Regexes are not valid
434// - Return [`Error::DateError`] if the start date is not before the end date
435fn report<F>(config: &Config, filter: &dyn DayFilter, mut f: F) -> crate::Result<()>
436where
437 F: FnMut(&Day) -> crate::Result<()>
438{
439 let start = filter.start();
440
441 let file = logfile(config)?.open()?;
442 let mut prev: Option<Entry> = TaskLineIter::new(buf_reader(file), &start, &filter.end())?
443 .last_line_before()
444 .and_then(|l| Entry::from_line(&l).ok())
445 .map(|e| e.to_day_end());
446
447 let mut day = start_day(&start, prev.as_ref())?;
448
449 let file = logfile(config)?.open()?;
450 for line in TaskLineIter::new(buf_reader(file), &start, &filter.end())? {
451 let entry = Entry::from_line(&line)?;
452 let stamp = entry.stamp();
453
454 if day.date_stamp() != stamp {
455 // Deal with end of day
456 if !day.is_complete() {
457 if let Some(prev_entry) = prev {
458 let day_end = prev_entry.to_day_end();
459 day.add_entry(day_end.clone())?;
460 prev = Some(day_end);
461 }
462 }
463 if let Some(filtered) = filter.filter_day(day) {
464 f(&filtered)?;
465 }
466
467 day = start_day(&stamp, prev.as_ref())?;
468 }
469 day.add_entry(entry.clone())?;
470 prev = Some(entry);
471 }
472 day.finish()?;
473
474 if let Some(filtered) = filter.filter_day(day) {
475 f(&filtered)?;
476 }
477 Ok(())
478}
479
480// Return a file object for the report file.
481fn report_file(filename: &str) -> crate::Result<fs::File> {
482 Ok(fs::OpenOptions::new()
483 .create(true)
484 .write(true)
485 .truncate(true)
486 .open(filename)
487 .map_err(|e| PathError::FileAccess(filename.to_string(), e.to_string()))?)
488}
489
490// Display the chart in the configured browser.
491fn launch_chart(config: &Config, filename: &str) -> crate::Result<()> {
492 Command::new(config.browser())
493 .arg(filename)
494 .stderr(Stdio::null()) // discard output, for odd chromium message
495 .status()
496 .map_err(|e| Error::EditorFailed(config.browser().to_string(), e.to_string()))?;
497 Ok(())
498}
499
500/// Create a graphical chart for each supplied day.
501///
502/// # Errors
503///
504/// - Return [`PathError::FilenameMissing`] if the log file is missing.
505/// - Return [`PathError::InvalidPath`] if the path part of the log file is not a valid path.
506/// - Return [`PathError::FileAccess`] if the file cannot be opened.
507/// - Return [`Error::BadProjectFilter`] if the supplied project Regexes are not valid
508/// - Return [`Error::DateError`] if the start date is not before the end date
509pub fn chart_daily(config: &Config, dates: &[String]) -> crate::Result<()> {
510 let filename = config.reportfile();
511 let mut file = report_file(&filename)?;
512 let mut w = EmitterConfig::new()
513 .perform_indent(true)
514 .write_document_declaration(false)
515 .create_writer(&mut file);
516 emit_xml!(&mut w, html => {
517 emit_xml!(w, head => {
518 emit_xml!(w, title; "Daily Timelog Report")?;
519 emit_xml!(w, style, type: "text/css"; STYLESHEET)
520 })?;
521 emit_xml!(w, body => {
522 report(config, &DateRangeArgs::new(dates)?, |day|
523 day.daily_chart().write(&mut w)
524 )
525 })
526 })?;
527
528 launch_chart(config, &filename)
529}
530
531/// Print the full daily report for each supplied day.
532///
533/// # Errors
534///
535/// - Return [`PathError::FilenameMissing`] if the log file is missing.
536/// - Return [`PathError::InvalidPath`] if the path part of the log file is not a valid path.
537/// - Return [`PathError::FileAccess`] if the file cannot be opened.
538/// - Return [`Error::BadProjectFilter`] if the supplied project Regexes are not valid
539/// - Return [`Error::DateError`] if the start date is not before the end date
540pub fn report_daily(config: &Config, dates: &[String], projs: &[String]) -> crate::Result<()> {
541 report(config, &FilterArgs::new(dates, projs)?, |day| {
542 print!("{}", day.detail_report());
543 Ok(())
544 })
545}
546
547/// Print the summary report for each supplied day.
548///
549/// # Errors
550///
551/// - Return [`PathError::FilenameMissing`] if the log file is missing.
552/// - Return [`PathError::InvalidPath`] if the path part of the log file is not a valid path.
553/// - Return [`PathError::FileAccess`] if the file cannot be opened.
554/// - Return [`Error::BadProjectFilter`] if the supplied project Regexes are not valid
555/// - Return [`Error::DateError`] if the start date is not before the end date
556pub fn report_summary(config: &Config, dates: &[String], projs: &[String]) -> crate::Result<()> {
557 report(config, &FilterArgs::new(dates, projs)?, |day| {
558 print!("{}", day.summary_report());
559 Ok(())
560 })
561}
562
563/// Print the hourly report for each supplied day.
564///
565/// # Errors
566///
567/// - Return [`PathError::FilenameMissing`] if the log file is missing.
568/// - Return [`PathError::InvalidPath`] if the path part of the log file is not a valid path.
569/// - Return [`PathError::FileAccess`] if the file cannot be opened.
570/// - Return [`Error::BadProjectFilter`] if the supplied project Regexes are not valid
571/// - Return [`Error::DateError`] if the start date is not before the end date
572pub fn report_hours(config: &Config, dates: &[String], projs: &[String]) -> crate::Result<()> {
573 report(config, &FilterArgs::new(dates, projs)?, |day| {
574 print!("{}", day.hours_report());
575 Ok(())
576 })
577}
578
579/// Print a report of the zero duration events for each supplied day.
580///
581/// # Errors
582///
583/// - Return [`PathError::FilenameMissing`] if the log file is missing.
584/// - Return [`PathError::InvalidPath`] if the path part of the log file is not a valid path.
585/// - Return [`PathError::FileAccess`] if the file cannot be opened.
586/// - Return [`Error::BadProjectFilter`] if the supplied project Regexes are not valid
587/// - Return [`Error::DateError`] if the start date is not before the end date
588pub fn report_events(
589 config: &Config, dates: &[String], projs: &[String], compact: bool
590) -> crate::Result<()> {
591 report(config, &FilterArgs::new(dates, projs)?, |day| {
592 print!("{}", day.event_report(compact));
593 Ok(())
594 })
595}
596
597/// Print a report of intervals between the zero duration events for each supplied day.
598///
599/// # Errors
600///
601/// - Return [`PathError::FilenameMissing`] if the log file is missing.
602/// - Return [`PathError::InvalidPath`] if the path part of the log file is not a valid path.
603/// - Return [`PathError::FileAccess`] if the file cannot be opened.
604/// - Return [`Error::BadProjectFilter`] if the supplied project Regexes are not valid
605/// - Return [`Error::DateError`] if the start date is not before the end date
606pub fn report_intervals(config: &Config, dates: &[String], projs: &[String]) -> crate::Result<()> {
607 let mut events: Vec<Entry> = vec![];
608 let filter = FilterArgs::new(dates, projs)?;
609 report(config, &filter, |day| {
610 events.append(&mut day.events().cloned().collect());
611 Ok(())
612 })?;
613 if events.is_empty() { return Ok(()); }
614
615 // fake an event for current time.
616 events.push(Entry::new_marked("now", DateTime::now(), EntryKind::Event));
617 for [first, second] in events.array_windows() {
618 #[rustfmt::skip]
619 println!("{} {} : {}",
620 first.timestamp(),
621 first.entry_text(),
622 format_dur(&(second.date_time() - first.date_time())?)
623 );
624 }
625 Ok(())
626}
627
628/// Display the current task
629///
630/// # Errors
631///
632/// - Return [`PathError::FilenameMissing`] if the log file is missing.
633/// - Return [`PathError::InvalidPath`] if the path part of the log file is not a valid path.
634/// - Return [`PathError::FileAccess`] if the file cannot be opened.
635pub fn current_task(config: &Config) -> crate::Result<()> {
636 let entry = logfile(config)?.last_entry()?;
637 if entry.is_stop() {
638 println!("Not in entry.");
639 }
640 else {
641 println!("{}", &entry);
642 let dur = (DateTime::now() - entry.date_time())?;
643 println!("Duration: {}", format_dur(&dur));
644 }
645
646 Ok(())
647}
648
649/// Archive the first year from the logfile (if not the current year).
650///
651/// # Errors
652///
653/// - Return [`Error::PathError`] for any error accessing the log or archive files.
654pub fn archive_year(config: &Config) -> crate::Result<()> {
655 match Archiver::new(config).archive()? {
656 None => println!("Nothing to archive"),
657 Some(year) => println!("{year} archived")
658 }
659 Ok(())
660}
661
662/// List the aliases from the config file.
663pub fn list_aliases(config: &Config) {
664 let mut aliases: Vec<&str> = config.alias_names().map(String::as_str).collect();
665 aliases.as_mut_slice().sort();
666 let maxlen: usize = aliases.iter().map(|a| a.len()).max().unwrap_or_default();
667
668 println!("Aliases:");
669 for alias in aliases {
670 if let Some(value) = config.alias(alias) {
671 println!(" {1:0$} : {2}", &maxlen, &alias, value);
672 }
673 }
674}
675
676/// Check the logfile for any problems
677///
678/// # Errors
679///
680/// - Return [`PathError::FilenameMissing`] if the `file` has no filename.
681/// - Return [`PathError::InvalidPath`] if the path part of `file` is not a valid path.
682pub fn check_logfile(config: &Config) -> crate::Result<()> {
683 let problems = logfile(config)?.problems();
684
685 if problems.is_empty() {
686 println!("No problems found");
687 }
688 else {
689 for p in problems {
690 println!("{p}");
691 }
692 }
693
694 Ok(())
695}
696
697/// Swap the current task with the top item on the stack.
698///
699/// # Errors
700///
701/// - Return [`PathError::FilenameMissing`] if the `file` has no filename.
702/// - Return [`PathError::InvalidPath`] if the path part of `file` is not a valid path.
703pub fn swap_entry(config: &Config) -> crate::Result<()> {
704 let stack = stack(config)?;
705 let logfile = logfile(config)?;
706 if let Some(curr_task) = logfile.last_line() {
707 if let Some(task) = stack.pop() {
708 logfile.add_task(&task)?;
709 }
710 let entry = Entry::from_line(&curr_task)?;
711 if !entry.is_stop() {
712 stack.push(entry.entry_text())?;
713 }
714 }
715 Ok(())
716}
717
718/// Remove all but the top items on the stack
719///
720/// # Errors
721///
722/// - Return [`PathError::FilenameMissing`] if the `file` has no filename.
723/// - Return [`PathError::InvalidPath`] if the path part of `file` is not a valid path.
724pub fn stack_keep(config: &Config, num: NonZeroU32) -> crate::Result<()> {
725 let stack = stack(config)?;
726 stack.keep(num)
727}
728
729/// Clear the stack
730///
731/// # Errors
732///
733/// - Return [`PathError::FilenameMissing`] if the `file` has no filename.
734/// - Return [`PathError::InvalidPath`] if the path part of `file` is not a valid path.
735pub fn stack_clear(config: &Config) -> crate::Result<()> {
736 let stack = stack(config)?;
737 stack.clear().map_err(Into::into)
738}
739
740/// Clear the stack
741///
742/// # Errors
743///
744/// - Return [`PathError::FilenameMissing`] if the `file` has no filename.
745/// - Return [`PathError::InvalidPath`] if the path part of `file` is not a valid path.
746pub fn stack_drop(config: &Config, num: NonZeroU32) -> crate::Result<()> {
747 let stack = stack(config)?;
748 stack.drop(num)
749}
750
751/// Display the top item on the stack
752///
753/// # Errors
754///
755/// - Return [`PathError::FilenameMissing`] if the `file` has no filename.
756/// - Return [`PathError::InvalidPath`] if the path part of `file` is not a valid path.
757pub fn stack_top(config: &Config) -> crate::Result<()> {
758 let stack = stack(config)?;
759 println!("{}", stack.top()?);
760 Ok(())
761}