refactor(cli): consolidate output through output module

- Add header(), detail(), item(), blank() functions to output.rs
- Replace all raw println!() calls with output::* equivalents
- Fix {:?} debug format for paths — use .display() for clean output
- Extract print_tasks() helper to deduplicate task display logic
- Consistent formatting: info for empty states, item for list entries
This commit is contained in:
Tristan Michael 2026-03-30 16:29:57 -07:00 committed by GitButler
parent c32a6fbe8b
commit b863e025d5
6 changed files with 64 additions and 75 deletions

View file

@ -35,7 +35,7 @@ pub fn execute(path: String, name: String) -> Result<()> {
config.save_to_file(&config_path) config.save_to_file(&config_path)
.context("Failed to save config")?; .context("Failed to save config")?;
output::success(&format!("Initialized workspace \"{}\" at {:?}", name, path_buf)); output::success(&format!("Initialized workspace \"{}\" at {}", name, path_buf.display()));
output::success("Created default list \"My Tasks\""); output::success("Created default list \"My Tasks\"");
output::success(&format!("Set \"{}\" as current workspace", name)); output::success(&format!("Set \"{}\" as current workspace", name));

View file

@ -1,8 +1,21 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use colored::*; use colored::*;
use bevy_tasks_core::{Task, TaskStatus};
use crate::output; use crate::output;
use crate::commands::get_repository; use crate::commands::get_repository;
fn print_tasks(tasks: &[Task]) {
if tasks.is_empty() {
output::item("No tasks");
return;
}
for task in tasks {
let checkbox = if task.status == TaskStatus::Completed { "[✓]".green() } else { "[ ]".normal() };
let due_str = task.due_date.map(|d| format!(" (due: {})", d.format("%Y-%m-%d")).yellow().to_string()).unwrap_or_default();
output::item(&format!("{} {}{} {}", checkbox, task.title, due_str, task.id.to_string().dimmed()));
}
}
pub fn create(name: String, workspace: Option<String>) -> Result<()> { pub fn create(name: String, workspace: Option<String>) -> Result<()> {
let (mut repo, _workspace_name) = get_repository(workspace)?; let (mut repo, _workspace_name) = get_repository(workspace)?;
@ -21,7 +34,7 @@ pub fn show(list_name: Option<String>, workspace: Option<String>) -> Result<()>
.context("Failed to get lists")?; .context("Failed to get lists")?;
if lists.is_empty() { if lists.is_empty() {
println!("No lists found. Create one with 'bevy-tasks list create <name>'"); output::info("No lists found. Create one with 'bevy-tasks list create <name>'");
return Ok(()); return Ok(());
} }
@ -31,54 +44,14 @@ pub fn show(list_name: Option<String>, workspace: Option<String>) -> Result<()>
.find(|l| l.title == name) .find(|l| l.title == name)
.ok_or_else(|| anyhow::anyhow!("List '{}' not found", name))?; .ok_or_else(|| anyhow::anyhow!("List '{}' not found", name))?;
println!("{} {} {}", list.title.bold(), format!("({} tasks)", list.tasks.len()).dimmed(), ""); output::header(&format!("{} ({})", list.title, format!("{} tasks", list.tasks.len()).dimmed()));
print_tasks(&list.tasks);
if list.tasks.is_empty() {
println!(" No tasks");
} else {
for task in &list.tasks {
let checkbox = if task.status == bevy_tasks_core::TaskStatus::Completed {
"[✓]".green()
} else {
"[ ]".normal()
};
let due_str = if let Some(due) = task.due_date {
format!(" (due: {})", due.format("%Y-%m-%d")).yellow().to_string()
} else {
String::new()
};
let id_str = task.id.to_string();
println!(" {} {}{} {}", checkbox, task.title, due_str, id_str.dimmed());
}
}
} else { } else {
// Show all lists // Show all lists
for list in &lists { for list in &lists {
println!("{} {}", list.title.bold(), format!("({} tasks)", list.tasks.len()).dimmed()); output::header(&format!("{} ({})", list.title, format!("{} tasks", list.tasks.len()).dimmed()));
print_tasks(&list.tasks);
if list.tasks.is_empty() { output::blank();
println!(" No tasks");
} else {
for task in &list.tasks {
let checkbox = if task.status == bevy_tasks_core::TaskStatus::Completed {
"[✓]".green()
} else {
"[ ]".normal()
};
let due_str = if let Some(due) = task.due_date {
format!(" (due: {})", due.format("%Y-%m-%d")).yellow().to_string()
} else {
String::new()
};
let id_str = task.id.to_string();
println!(" {} {}{} {}", checkbox, task.title, due_str, id_str.dimmed());
}
}
println!();
} }
} }
@ -105,7 +78,7 @@ pub fn delete(name: String, workspace: Option<String>) -> Result<()> {
io::stdin().read_line(&mut input)?; io::stdin().read_line(&mut input)?;
if input.trim().to_lowercase() != "y" { if input.trim().to_lowercase() != "y" {
println!("Cancelled"); output::info("Cancelled");
return Ok(()); return Ok(());
} }

View file

@ -21,8 +21,8 @@ pub fn setup(workspace_name: Option<String>) -> Result<()> {
}; };
// Prompt for WebDAV URL // Prompt for WebDAV URL
println!("WebDAV sync setup for workspace \"{}\"", name.green()); output::header(&format!("WebDAV sync setup for workspace \"{}\"", name.green()));
println!(); output::blank();
let url = prompt("WebDAV URL: ")?; let url = prompt("WebDAV URL: ")?;
if url.is_empty() { if url.is_empty() {
@ -35,8 +35,8 @@ pub fn setup(workspace_name: Option<String>) -> Result<()> {
.context("Failed to read password")?; .context("Failed to read password")?;
// Test connection // Test connection
println!(); output::blank();
println!("Testing connection..."); output::info("Testing connection...");
let rt = tokio::runtime::Runtime::new().context("Failed to create async runtime")?; let rt = tokio::runtime::Runtime::new().context("Failed to create async runtime")?;
let client = WebDavClient::new(&url, &username, &password); let client = WebDavClient::new(&url, &username, &password);
@ -102,7 +102,7 @@ pub fn execute(mode: SyncMode, workspace_name: Option<String>) -> Result<()> {
SyncMode::Push => "Pushing", SyncMode::Push => "Pushing",
SyncMode::Pull => "Pulling", SyncMode::Pull => "Pulling",
}; };
println!("{} workspace \"{}\"...", mode_str, name.green()); output::info(&format!("{} workspace \"{}\"...", mode_str, name.green()));
let rt = tokio::runtime::Runtime::new().context("Failed to create async runtime")?; let rt = tokio::runtime::Runtime::new().context("Failed to create async runtime")?;
let result = rt.block_on(sync_workspace( let result = rt.block_on(sync_workspace(
@ -153,7 +153,7 @@ pub fn status(workspace_name: Option<String>, all: bool) -> Result<()> {
if ws.webdav_url.is_some() { if ws.webdav_url.is_some() {
found_any = true; found_any = true;
print_workspace_status(&name, &ws.path, ws.webdav_url.as_deref())?; print_workspace_status(&name, &ws.path, ws.webdav_url.as_deref())?;
println!(); output::blank();
} }
} }
if !found_any { if !found_any {
@ -178,27 +178,27 @@ pub fn status(workspace_name: Option<String>, all: bool) -> Result<()> {
} }
fn print_workspace_status(name: &str, path: &std::path::Path, webdav_url: Option<&str>) -> Result<()> { fn print_workspace_status(name: &str, path: &std::path::Path, webdav_url: Option<&str>) -> Result<()> {
println!("Workspace: {}", name.green()); output::header(&format!("Workspace: {}", name.green()));
if let Some(url) = webdav_url { if let Some(url) = webdav_url {
println!(" WebDAV URL: {}", url); output::detail("WebDAV URL", url);
} else { } else {
println!(" WebDAV: {}", "not configured".dimmed()); output::detail("WebDAV", &"not configured".dimmed().to_string());
return Ok(()); return Ok(());
} }
let info = get_sync_status(path)?; let info = get_sync_status(path)?;
if let Some(last) = info.last_sync { if let Some(last) = info.last_sync {
println!(" Last sync: {}", last.format("%Y-%m-%d %H:%M:%S UTC")); output::detail("Last sync", &last.format("%Y-%m-%d %H:%M:%S UTC").to_string());
} else { } else {
println!(" Last sync: {}", "never".dimmed()); output::detail("Last sync", &"never".dimmed().to_string());
} }
println!(" Tracked files: {}", info.tracked_files); output::detail("Tracked files", &info.tracked_files.to_string());
println!(" Pending changes: {}", info.pending_changes); output::detail("Pending changes", &info.pending_changes.to_string());
if info.queued_operations > 0 { if info.queued_operations > 0 {
println!(" Queued operations: {}", format!("{}", info.queued_operations).yellow()); output::detail("Queued operations", &format!("{}", info.queued_operations).yellow().to_string());
} }
Ok(()) Ok(())

View file

@ -100,7 +100,7 @@ pub fn delete(task_id_str: String, workspace: Option<String>) -> Result<()> {
let mut input = String::new(); let mut input = String::new();
io::stdin().read_line(&mut input)?; io::stdin().read_line(&mut input)?;
if input.trim().to_lowercase() != "y" { if input.trim().to_lowercase() != "y" {
println!("Cancelled"); output::info("Cancelled");
return Ok(()); return Ok(());
} }

View file

@ -38,7 +38,7 @@ pub fn add(name: String, path: String) -> Result<()> {
// Save config // Save config
save_config(&config)?; save_config(&config)?;
output::success(&format!("Added workspace \"{}\" at {:?}", name, path_buf)); output::success(&format!("Added workspace \"{}\" at {}", name, path_buf.display()));
output::success("Created default list \"My Tasks\""); output::success("Created default list \"My Tasks\"");
Ok(()) Ok(())
@ -48,7 +48,7 @@ pub fn list() -> Result<()> {
let config = load_config()?; let config = load_config()?;
if config.workspaces.is_empty() { if config.workspaces.is_empty() {
println!("No workspaces configured. Use 'bevy-tasks init' to create one."); output::info("No workspaces configured. Use 'bevy-tasks init' to create one.");
return Ok(()); return Ok(());
} }
@ -63,7 +63,7 @@ pub fn list() -> Result<()> {
} else { } else {
"".normal() "".normal()
}; };
println!(" {}: {:?}{}", name, workspace_config.path, marker); output::item(&format!("{}: {}{}", name, workspace_config.path.display(), marker));
} }
Ok(()) Ok(())
@ -103,7 +103,7 @@ pub fn remove(name: String) -> Result<()> {
io::stdin().read_line(&mut input)?; io::stdin().read_line(&mut input)?;
if input.trim().to_lowercase() != "y" { if input.trim().to_lowercase() != "y" {
println!("Cancelled"); output::info("Cancelled");
return Ok(()); return Ok(());
} }
@ -134,7 +134,7 @@ pub fn retarget(name: String, path: String) -> Result<()> {
config.add_workspace(name.clone(), WorkspaceConfig::new(path_buf.clone())); config.add_workspace(name.clone(), WorkspaceConfig::new(path_buf.clone()));
save_config(&config)?; save_config(&config)?;
output::success(&format!("Workspace \"{}\" now points to {:?}", name, path_buf)); output::success(&format!("Workspace \"{}\" now points to {}", name, path_buf.display()));
Ok(()) Ok(())
} }
@ -155,7 +155,7 @@ pub fn migrate(name: String, new_path: String) -> Result<()> {
.path.clone(); .path.clone();
// Confirm // Confirm
output::warning(&format!("This will move all files from {:?} to {:?}", old_path, new_path_buf)); output::warning(&format!("This will move all files from {} to {}", old_path.display(), new_path_buf.display()));
print!("Continue? (y/n): "); print!("Continue? (y/n): ");
use std::io::{self, Write}; use std::io::{self, Write};
io::stdout().flush()?; io::stdout().flush()?;
@ -164,7 +164,7 @@ pub fn migrate(name: String, new_path: String) -> Result<()> {
io::stdin().read_line(&mut input)?; io::stdin().read_line(&mut input)?;
if input.trim().to_lowercase() != "y" { if input.trim().to_lowercase() != "y" {
println!("Cancelled"); output::info("Cancelled");
return Ok(()); return Ok(());
} }
@ -172,7 +172,7 @@ pub fn migrate(name: String, new_path: String) -> Result<()> {
std::fs::create_dir_all(&new_path_buf)?; std::fs::create_dir_all(&new_path_buf)?;
// Move files // Move files
println!("Moving files..."); output::info("Moving files...");
let entries = std::fs::read_dir(&old_path)?; let entries = std::fs::read_dir(&old_path)?;
let mut count = 0; let mut count = 0;
@ -185,10 +185,10 @@ pub fn migrate(name: String, new_path: String) -> Result<()> {
let mut options = fs_extra::dir::CopyOptions::new(); let mut options = fs_extra::dir::CopyOptions::new();
options.copy_inside = true; options.copy_inside = true;
fs_extra::dir::move_dir(entry.path(), &new_path_buf, &options)?; fs_extra::dir::move_dir(entry.path(), &new_path_buf, &options)?;
println!(" Moved {:?}/", file_name); output::item(&format!("Moved {}/", file_name.to_string_lossy()));
} else { } else {
std::fs::rename(entry.path(), dest)?; std::fs::rename(entry.path(), dest)?;
println!(" Moved {:?}", file_name); output::item(&format!("Moved {}", file_name.to_string_lossy()));
} }
count += 1; count += 1;
} }
@ -202,8 +202,8 @@ pub fn migrate(name: String, new_path: String) -> Result<()> {
config.add_workspace(name.clone(), WorkspaceConfig::new(new_path_buf.clone())); config.add_workspace(name.clone(), WorkspaceConfig::new(new_path_buf.clone()));
save_config(&config)?; save_config(&config)?;
output::success(&format!("Migrated {} items to {:?}", count, new_path_buf)); output::success(&format!("Migrated {} items to {}", count, new_path_buf.display()));
output::success(&format!("Workspace \"{}\" now points to {:?}", name, new_path_buf)); output::success(&format!("Workspace \"{}\" now points to {}", name, new_path_buf.display()));
Ok(()) Ok(())
} }

View file

@ -15,3 +15,19 @@ pub fn warning(message: &str) {
pub fn info(message: &str) { pub fn info(message: &str) {
println!("{} {}", "".blue(), message); println!("{} {}", "".blue(), message);
} }
pub fn header(message: &str) {
println!("{}", message.bold());
}
pub fn detail(label: &str, value: &str) {
println!(" {}: {}", label, value);
}
pub fn item(message: &str) {
println!(" {}", message);
}
pub fn blank() {
println!();
}