A file-based task manager
0
fork

Configure Feed

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

fzf-pick the namespace when tsk switch is run with no name

Implements tsk-3: switching with no argument lists existing namespaces
through fzf with the active one marked, plus a <new> sentinel for
creating one on the fly. namespace switch behaves the same way.

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

+105 -5
+105 -5
src/main.rs
··· 162 162 #[command(subcommand)] 163 163 action: QueueAction, 164 164 }, 165 - /// Switch active namespace (shorthand). 166 - Switch { name: String }, 165 + /// Switch active namespace (shorthand). With no name, fzf-picks from 166 + /// existing namespaces (plus a `<new>` sentinel for creating one on 167 + /// the fly). 168 + Switch { name: Option<String> }, 167 169 /// Generate shell completion. 168 170 Completion { 169 171 #[arg(short = 's')] ··· 175 177 enum NamespaceAction { 176 178 List, 177 179 Current, 178 - Switch { name: String }, 180 + /// Switch active namespace. With no name, fzf-picks from existing 181 + /// namespaces (plus a `<new>` sentinel for creating one on the fly). 182 + Switch { name: Option<String> }, 179 183 } 180 184 181 185 #[derive(Subcommand)] ··· 281 285 Commands::Reject { key, remote } => command_reject(dir, key, remote), 282 286 Commands::Namespace { action } => command_namespace(dir, action), 283 287 Commands::Queue { action } => command_queue(dir, action), 284 - Commands::Switch { name } => Workspace::from_path(dir)?.switch_namespace(&name), 288 + Commands::Switch { name } => command_namespace_switch(dir, name), 285 289 Commands::Completion { shell } => { 286 290 generate(shell, &mut Cli::command(), "tsk", &mut io::stdout()); 287 291 Ok(()) ··· 505 509 } 506 510 } 507 511 NamespaceAction::Current => println!("{}", ws.namespace()), 508 - NamespaceAction::Switch { name } => ws.switch_namespace(&name)?, 512 + NamespaceAction::Switch { name } => return resolve_and_switch_namespace(&ws, name), 509 513 } 510 514 Ok(()) 511 515 } ··· 528 532 Ok(()) 529 533 } 530 534 535 + const NEW_NS_SENTINEL: &str = "<new>"; 536 + 537 + fn command_namespace_switch(dir: PathBuf, name: Option<String>) -> Result<()> { 538 + let ws = Workspace::from_path(dir)?; 539 + resolve_and_switch_namespace(&ws, name) 540 + } 541 + 542 + fn resolve_and_switch_namespace(ws: &Workspace, name: Option<String>) -> Result<()> { 543 + let target = match name { 544 + Some(n) => n, 545 + None => pick_namespace(ws)?, 546 + }; 547 + ws.switch_namespace(&target)?; 548 + println!("Switched to namespace '{target}'"); 549 + Ok(()) 550 + } 551 + 552 + fn pick_namespace(ws: &Workspace) -> Result<String> { 553 + let cur = ws.namespace(); 554 + let existing = ws.list_namespaces()?; 555 + let entries = namespace_picker_entries(&existing, &cur); 556 + let picked = fzf::select::<_, String, _>(entries, ["--prompt=namespace> "])? 557 + .ok_or_else(|| errors::Error::Parse("No namespace selected".into()))?; 558 + let picked = strip_picker_marker(&picked); 559 + if picked == NEW_NS_SENTINEL { 560 + let name = prompt_line("New namespace name: ")?; 561 + if name.is_empty() { 562 + return Err(errors::Error::Parse("Empty namespace name".into())); 563 + } 564 + Ok(name) 565 + } else { 566 + Ok(picked.to_string()) 567 + } 568 + } 569 + 570 + /// Build the fzf input lines for namespace selection: every existing 571 + /// namespace (active marked with `* `, others with ` `) plus a trailing 572 + /// `<new>` sentinel for creating one on the fly. The active namespace is 573 + /// always present even when no refs have been written yet. 574 + fn namespace_picker_entries(existing: &[String], current: &str) -> Vec<String> { 575 + let mut entries: Vec<String> = existing 576 + .iter() 577 + .map(|n| { 578 + if n == current { 579 + format!("* {n}") 580 + } else { 581 + format!(" {n}") 582 + } 583 + }) 584 + .collect(); 585 + if !existing.iter().any(|n| n == current) { 586 + entries.insert(0, format!("* {current}")); 587 + } 588 + entries.push(NEW_NS_SENTINEL.to_string()); 589 + entries 590 + } 591 + 592 + fn strip_picker_marker(s: &str) -> &str { 593 + s.strip_prefix("* ").or_else(|| s.strip_prefix(" ")).unwrap_or(s) 594 + } 595 + 596 + fn prompt_line(prompt: &str) -> Result<String> { 597 + eprint!("{prompt}"); 598 + io::stderr().flush()?; 599 + let mut s = String::new(); 600 + io::stdin().read_line(&mut s)?; 601 + Ok(s.trim_end_matches(['\n', '\r']).to_string()) 602 + } 603 + 531 604 #[allow(dead_code)] 532 605 fn _silence_unused(_w: &dyn Write, _t: Task) {} 606 + 607 + #[cfg(test)] 608 + mod tests { 609 + use super::*; 610 + 611 + #[test] 612 + fn picker_marks_current_and_appends_sentinel() { 613 + let entries = namespace_picker_entries( 614 + &["alpha".to_string(), "tsk".to_string()], 615 + "tsk", 616 + ); 617 + assert_eq!(entries, vec![" alpha", "* tsk", "<new>"]); 618 + } 619 + 620 + #[test] 621 + fn picker_includes_current_when_missing_from_list() { 622 + let entries = namespace_picker_entries(&[], "tsk"); 623 + assert_eq!(entries, vec!["* tsk", "<new>"]); 624 + } 625 + 626 + #[test] 627 + fn strip_marker_handles_all_prefixes() { 628 + assert_eq!(strip_picker_marker("* tsk"), "tsk"); 629 + assert_eq!(strip_picker_marker(" alpha"), "alpha"); 630 + assert_eq!(strip_picker_marker("<new>"), "<new>"); 631 + } 632 + }