use super::build_api_client;
use crate::{
api,
config::Config,
models::{SectionCreateData, SectionCreateRequest, SectionUpdateData, SectionUpdateRequest},
output::OutputFormat,
};
use anyhow::{Context, Result, bail};
use clap::{Args, Subcommand};
use dialoguer::Confirm;
use std::io::{IsTerminal, stdout};
use tokio::runtime::Builder as RuntimeBuilder;
#[derive(Subcommand, Debug)]
pub enum SectionCommand {
List(SectionListArgs),
Show(SectionShowArgs),
Create(SectionCreateArgs),
Update(SectionUpdateArgs),
Delete(SectionDeleteArgs),
Tasks {
#[command(subcommand)]
command: SectionTasksCommand,
},
}
#[derive(Subcommand, Debug)]
pub enum SectionTasksCommand {
List(SectionTasksArgs),
}
#[derive(Args, Debug)]
pub struct SectionListArgs {
#[arg(long)]
pub project: String,
#[arg(long, value_enum)]
pub output: Option<OutputFormat>,
}
#[derive(Args, Debug)]
pub struct SectionShowArgs {
#[arg(value_name = "SECTION")]
pub section: String,
#[arg(long, value_name = "FIELD")]
pub fields: Vec<String>,
#[arg(long, value_enum)]
pub output: Option<OutputFormat>,
}
#[derive(Args, Debug)]
pub struct SectionCreateArgs {
#[arg(long)]
pub name: String,
#[arg(long)]
pub project: String,
#[arg(long = "insert-before")]
pub insert_before: Option<String>,
#[arg(long = "insert-after")]
pub insert_after: Option<String>,
#[arg(long, value_enum)]
pub output: Option<OutputFormat>,
}
#[derive(Args, Debug)]
pub struct SectionUpdateArgs {
#[arg(value_name = "SECTION")]
pub section: String,
#[arg(long)]
pub name: Option<String>,
#[arg(long, value_enum)]
pub output: Option<OutputFormat>,
}
#[derive(Args, Debug)]
pub struct SectionDeleteArgs {
#[arg(value_name = "SECTION")]
pub section: String,
#[arg(long)]
pub force: bool,
}
#[derive(Args, Debug)]
pub struct SectionTasksArgs {
#[arg(value_name = "SECTION")]
pub section: String,
#[arg(long, value_name = "FIELD")]
pub fields: Vec<String>,
#[arg(long, value_enum)]
pub output: Option<OutputFormat>,
}
pub fn execute_section_command(cmd: SectionCommand, config: &Config) -> Result<()> {
let runtime = RuntimeBuilder::new_current_thread()
.enable_all()
.build()
.context("failed to build tokio runtime")?;
runtime.block_on(async { execute_section_command_async(cmd, config).await })
}
async fn execute_section_command_async(cmd: SectionCommand, config: &Config) -> Result<()> {
match cmd {
SectionCommand::List(args) => list_sections(args, config).await,
SectionCommand::Show(args) => show_section(args, config).await,
SectionCommand::Create(args) => create_section(args, config).await,
SectionCommand::Update(args) => update_section(args, config).await,
SectionCommand::Delete(args) => delete_section(args, config).await,
SectionCommand::Tasks { command } => match command {
SectionTasksCommand::List(args) => list_section_tasks(args, config).await,
},
}
}
fn determine_output(value: Option<OutputFormat>) -> OutputFormat {
value.unwrap_or_else(|| {
if stdout().is_terminal() {
OutputFormat::Table
} else {
OutputFormat::Json
}
})
}
async fn list_sections(args: SectionListArgs, config: &Config) -> Result<()> {
let client = build_api_client(config)?;
let sections = api::list_sections(&client, &args.project).await?;
let format = determine_output(args.output);
match format {
OutputFormat::Json => {
let json = serde_json::to_string_pretty(§ions)?;
println!("{json}");
}
OutputFormat::Csv => {
println!("gid,name,project_gid,project_name");
for section in sections {
let project_gid = section.project.as_ref().map_or("", |p| p.gid.as_str());
let project_name = section
.project
.as_ref()
.and_then(|p| p.name.as_deref())
.unwrap_or("");
println!(
"{},{},{},{}",
section.gid, section.name, project_gid, project_name
);
}
}
OutputFormat::Markdown => {
println!("| GID | Name | Project |");
println!("|---|---|---|");
for section in sections {
let project_label = section.project.as_ref().map_or_else(
|| "N/A".to_string(),
super::super::models::section::SectionProjectReference::label,
);
println!("| {} | {} | {} |", section.gid, section.name, project_label);
}
}
OutputFormat::Table => {
if sections.is_empty() {
println!("No sections found in project.");
} else {
println!("{:<20} {:<30} {:<20}", "GID", "NAME", "PROJECT");
println!("{}", "-".repeat(72));
for section in sections {
let project_label = section.project.as_ref().map_or_else(
|| "N/A".to_string(),
super::super::models::section::SectionProjectReference::label,
);
println!(
"{:<20} {:<30} {:<20}",
section.gid, section.name, project_label
);
}
}
}
}
Ok(())
}
fn render_section_detail(section: &crate::models::Section) {
println!("Section: {}", section.name);
println!("GID: {}", section.gid);
if let Some(project) = §ion.project {
println!(
"Project: {} ({})",
project.name.as_deref().unwrap_or("N/A"),
project.gid
);
}
if let Some(created_at) = §ion.created_at {
println!("Created: {created_at}");
}
}
async fn show_section(args: SectionShowArgs, config: &Config) -> Result<()> {
let client = build_api_client(config)?;
let section = api::get_section(&client, &args.section, args.fields).await?;
let format = determine_output(args.output);
if matches!(format, OutputFormat::Json) {
let json = serde_json::to_string_pretty(§ion)?;
println!("{json}");
} else {
render_section_detail(§ion);
}
Ok(())
}
async fn create_section(args: SectionCreateArgs, config: &Config) -> Result<()> {
let client = build_api_client(config)?;
let request = SectionCreateRequest {
data: SectionCreateData {
name: args.name,
insert_before: args.insert_before,
insert_after: args.insert_after,
},
};
let section = api::create_section(&client, &args.project, request).await?;
let format = determine_output(args.output);
if matches!(format, OutputFormat::Json) {
let json = serde_json::to_string_pretty(§ion)?;
println!("{json}");
} else {
println!("Created section: {}", section.name);
println!("GID: {}", section.gid);
if let Some(project) = §ion.project {
println!(
"Project: {} ({})",
project.name.as_deref().unwrap_or("N/A"),
project.gid
);
}
}
Ok(())
}
async fn update_section(args: SectionUpdateArgs, config: &Config) -> Result<()> {
let client = build_api_client(config)?;
if args.name.is_none() {
bail!("no updates specified; supply --name");
}
let request = SectionUpdateRequest {
data: SectionUpdateData { name: args.name },
};
let section = api::update_section(&client, &args.section, request).await?;
let format = determine_output(args.output);
if matches!(format, OutputFormat::Json) {
let json = serde_json::to_string_pretty(§ion)?;
println!("{json}");
} else {
println!("Updated section {}.", section.gid);
render_section_detail(§ion);
}
Ok(())
}
async fn delete_section(args: SectionDeleteArgs, config: &Config) -> Result<()> {
let client = build_api_client(config)?;
if !args.force {
let confirmed = Confirm::new()
.with_prompt(format!("Delete section {}?", args.section))
.default(false)
.interact()
.context("failed to read confirmation")?;
if !confirmed {
println!("Deletion aborted.");
return Ok(());
}
}
api::delete_section(&client, &args.section).await?;
println!("Deleted section {}.", args.section);
Ok(())
}
async fn list_section_tasks(args: SectionTasksArgs, config: &Config) -> Result<()> {
let client = build_api_client(config)?;
let tasks = api::get_section_tasks(&client, &args.section, args.fields).await?;
let format = determine_output(args.output);
match format {
OutputFormat::Json => {
let json = serde_json::to_string_pretty(&tasks)?;
println!("{json}");
}
OutputFormat::Csv => {
println!("gid,name,completed,assignee");
for task in tasks {
let assignee = task.assignee.as_ref().map_or_else(
|| "Unassigned".to_string(),
super::super::models::user::UserReference::label,
);
println!("{},{},{},{}", task.gid, task.name, task.completed, assignee);
}
}
OutputFormat::Markdown => {
println!("| GID | Name | Status | Assignee |");
println!("|---|---|---|---|");
for task in tasks {
let status = if task.completed { "Done" } else { "Open" };
let assignee = task.assignee.as_ref().map_or_else(
|| "Unassigned".to_string(),
super::super::models::user::UserReference::label,
);
println!(
"| {} | {} | {} | {} |",
task.gid, task.name, status, assignee
);
}
}
OutputFormat::Table => {
if tasks.is_empty() {
println!("No tasks found in section.");
} else {
println!(
"{:<20} {:<40} {:<10} {:<20}",
"GID", "NAME", "STATUS", "ASSIGNEE"
);
println!("{}", "-".repeat(92));
for task in tasks {
let status = if task.completed { "Done" } else { "Open" };
let assignee = task.assignee.as_ref().map_or_else(
|| "Unassigned".to_string(),
super::super::models::user::UserReference::label,
);
println!(
"{:<20} {:<40} {:<10} {:<20}",
task.gid, task.name, status, assignee
);
}
}
}
}
Ok(())
}