A file-based task manager
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Fuzzy-finder fallback for tsk export

`tsk export` with no -t/-T/-r drops into fzf showing every task in
the active namespace as `tsk-N\ttitle`; the picked id flows through
the existing export path.

Implementation reuses the shared TaskId clap struct rather than
adding bespoke per-command fields:

- TaskId.relative_id is now Option<u32> so "no flag passed at all"
is distinguishable from "explicitly -r 0".
- New TaskId::resolve_or_pick(ws) drops into fzf when nothing was
given. Existing commands that prefer "top of stack" silently keep
that behavior via TaskId → TaskIdentifier (relative_id defaults to 0
on conversion).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+40 -9
+40 -9
src/lib.rs
··· 315 315 id: Option<u32>, 316 316 #[arg(short = 'T', value_name = "TSK-ID", value_parser = parse_id)] 317 317 tsk_id: Option<Id>, 318 - #[arg(short = 'r', value_name = "RELATIVE", default_value_t = 0)] 319 - relative_id: u32, 318 + #[arg(short = 'r', value_name = "RELATIVE")] 319 + relative_id: Option<u32>, 320 + } 321 + 322 + impl TaskId { 323 + /// True when the user passed none of `-t`, `-T`, or `-r`. Commands 324 + /// that fall back to a fuzzy finder use this to decide whether to 325 + /// prompt; commands that prefer "top of stack" silently treat this 326 + /// as `Relative(0)` via the `From` impl. 327 + fn is_empty(&self) -> bool { 328 + self.id.is_none() && self.tsk_id.is_none() && self.relative_id.is_none() 329 + } 330 + 331 + /// Resolve to a `TaskIdentifier`, dropping into an fzf picker when no 332 + /// flag was supplied. Use when interactive selection is the desired 333 + /// fallback (e.g. `tsk export`); otherwise prefer `Into`, which 334 + /// silently picks the top of the stack. 335 + fn resolve_or_pick(self, ws: &Workspace) -> Result<TaskIdentifier> { 336 + if !self.is_empty() { 337 + return Ok(self.into()); 338 + } 339 + let entries = ws.list_namespace_tasks(&ws.namespace())?; 340 + if entries.is_empty() { 341 + return Err(errors::Error::NoTasks); 342 + } 343 + let lines: Vec<String> = entries 344 + .iter() 345 + .map(|e| format!("{}\t{}", e.id, e.title)) 346 + .collect(); 347 + let picked: Option<String> = fzf::select(lines, ["--prompt=task> "])?; 348 + let picked = picked.ok_or(errors::Error::NoTasks)?; 349 + let id_str = picked.split('\t').next().unwrap_or(""); 350 + let id: Id = 351 + parse_id(id_str).map_err(|e| errors::Error::Parse(e.to_string()))?; 352 + Ok(TaskIdentifier::Id(id)) 353 + } 320 354 } 321 355 322 356 impl From<TaskId> for TaskIdentifier { ··· 324 358 if let Some(id) = v.id.map(Id::from).or(v.tsk_id) { 325 359 TaskIdentifier::Id(id) 326 360 } else { 327 - TaskIdentifier::Relative(v.relative_id) 361 + TaskIdentifier::Relative(v.relative_id.unwrap_or(0)) 328 362 } 329 363 } 330 364 } ··· 366 400 Workspace::from_path(dir)?.deprioritize(task_id.into()) 367 401 } 368 402 Commands::Clean => Workspace::from_path(dir)?.clean(), 369 - Commands::Export { task_id, bind } => command_export(dir, task_id.into(), bind), 403 + Commands::Export { task_id, bind } => command_export(dir, task_id, bind), 370 404 Commands::Import { bind } => command_import(dir, bind), 371 405 Commands::Log { target } => command_log(dir, target), 372 406 Commands::FixUp => { ··· 665 699 Ok(()) 666 700 } 667 701 668 - fn command_export( 669 - dir: PathBuf, 670 - identifier: TaskIdentifier, 671 - bind: bool, 672 - ) -> Result<()> { 702 + fn command_export(dir: PathBuf, task_id: TaskId, bind: bool) -> Result<()> { 673 703 let ws = Workspace::from_path(dir)?; 704 + let identifier = task_id.resolve_or_pick(&ws)?; 674 705 let mbox = ws.export_task(identifier, bind)?; 675 706 print!("{mbox}"); 676 707 Ok(())