Skip to main content

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}