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