Something went wrong. Try again.
A Rust clone of doing by Brett Terpstra — a command line tool for remembering what you were doing and tracking what you've done. doing.aaronmallen.dev
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153use std::{ io::Write, process::{Command, Stdio},};
use doing_taskpaper::Entry;use log::warn;
use crate::Result;
/// Choose a single entry from a list using fzf (if available) or a dialoguer fallback.pub fn choose_entry(entries: &[Entry]) -> Result<Option<Entry>> { if has_fzf() { choose_fzf(entries) } else { warn!("fzf not found on $PATH, falling back to built-in menu"); choose_dialoguer(entries) }}
/// Convert a dialoguer error into a doing `Error`.pub fn dialoguer_error(e: impl std::fmt::Display) -> crate::Error { crate::Error::Io(std::io::Error::other(format!("input error: {e}")))}
pub fn has_fzf() -> bool { Command::new("fzf") .arg("--version") .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .is_ok_and(|s| s.success())}
/// Select multiple entries from a list using a dialoguer multi-select menu.pub fn select_entries(entries: &[Entry]) -> Result<Vec<Entry>> { let items: Vec<String> = entries.iter().map(format_entry).collect();
let selections = dialoguer::MultiSelect::new() .with_prompt("Select entries") .items(&items) .interact_opt() .map_err(dialoguer_error)?;
let Some(selections) = selections else { return Ok(Vec::new()); };
Ok(selections.into_iter().map(|i| entries[i].clone()).collect())}
fn choose_dialoguer(entries: &[Entry]) -> Result<Option<Entry>> { let items: Vec<String> = entries.iter().map(format_entry).collect();
let selection = dialoguer::Select::new() .with_prompt("Choose an entry") .items(&items) .interact_opt() .map_err(dialoguer_error)?;
Ok(selection.map(|i| entries[i].clone()))}
fn choose_fzf(entries: &[Entry]) -> Result<Option<Entry>> { let items: Vec<String> = entries.iter().map(format_entry).collect(); let input = items.join("\n");
let mut child = Command::new("fzf") .arg("--select-1") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::inherit()) .spawn() .map_err(crate::Error::Io)?;
if let Some(mut stdin) = child.stdin.take() { stdin.write_all(input.as_bytes()).map_err(crate::Error::Io)?; }
let output = child.wait_with_output().map_err(crate::Error::Io)?;
if !output.status.success() { return Ok(None); }
let chosen = String::from_utf8(output.stdout) .map_err(|e| crate::Error::Io(std::io::Error::other(e)))? .trim() .to_string(); let index = items.iter().position(|item| *item == chosen); Ok(index.map(|i| entries[i].clone()))}
fn format_entry(entry: &Entry) -> String { let date = entry.date().format(crate::cli::DONE_DATE_FORMAT); format!("{date} | {}", entry.full_title())}
#[cfg(test)]mod test { use super::*;
mod format_entry { use chrono::{Local, TimeZone}; use doing_taskpaper::{Note, Tag, Tags};
use super::*;
#[test] fn it_formats_entry_without_tags() { let entry = Entry::new( Local.with_ymd_and_hms(2024, 3, 17, 14, 0, 0).unwrap(), "Test task", Tags::new(), Note::new(), "Currently", None::<String>, );
let formatted = format_entry(&entry);
assert!(formatted.contains("Test task")); assert!(formatted.contains("2024-03-17")); }
#[test] fn it_formats_entry_with_tags() { let entry = Entry::new( Local.with_ymd_and_hms(2024, 3, 17, 14, 0, 0).unwrap(), "Test task", Tags::from_iter(vec![Tag::new("project", None::<String>)]), Note::new(), "Currently", None::<String>, );
let formatted = format_entry(&entry);
assert!(formatted.contains("Test task")); assert!(formatted.contains("@project")); } }
mod has_fzf { use super::*;
#[test] fn it_returns_a_bool() { let _ = has_fzf(); } }}