use std::io::Write;
use anyhow::{Context, Result};
use clap::Parser;
use crate::cli::drive::format::{output_as, sanitize_for_terminal, OutputFormat};
use crate::drive::account::{self, AccountSummary};
use crate::utils::settings::Settings;
#[derive(Parser)]
pub struct ListCommand {
#[arg(short = 'o', long, value_enum, default_value_t = OutputFormat::Table)]
pub output: OutputFormat,
}
impl ListCommand {
pub fn execute(self) -> Result<()> {
let settings = Settings::load().unwrap_or_default();
let accounts = account::list_accounts(&settings.drive);
run_list(&accounts, &self.output)
}
}
fn run_list(accounts: &[AccountSummary], output: &OutputFormat) -> Result<()> {
if output_as(&accounts.to_vec(), output)? {
return Ok(());
}
let stdout = std::io::stdout();
let mut handle = stdout.lock();
render_account_table(accounts, &mut handle)
}
fn render_account_table(accounts: &[AccountSummary], out: &mut dyn Write) -> Result<()> {
if accounts.is_empty() {
writeln!(
out,
"No named Drive accounts configured. Run `omni-dev drive auth login --account <name>` \
to create one."
)
.context("Failed to write empty-table message")?;
return Ok(());
}
let names: Vec<String> = accounts
.iter()
.map(|a| sanitize_for_terminal(&a.name))
.collect();
let name_width = "NAME"
.len()
.max(names.iter().map(String::len).max().unwrap_or(0));
let email_strings: Vec<String> = accounts
.iter()
.map(|a| sanitize_for_terminal(a.email_address.as_deref().unwrap_or("-")))
.collect();
let email_width = "EMAIL"
.len()
.max(email_strings.iter().map(String::len).max().unwrap_or(0));
let scope_strings: Vec<String> = accounts
.iter()
.map(|a| sanitize_for_terminal(a.scope.as_deref().unwrap_or("-")))
.collect();
let scope_width = "SCOPE"
.len()
.max(scope_strings.iter().map(String::len).max().unwrap_or(0));
writeln!(
out,
"{:<name_width$} {:<email_width$} {:<scope_width$} DEFAULT",
"NAME", "EMAIL", "SCOPE"
)
.context("Failed to write header row")?;
for (i, account) in accounts.iter().enumerate() {
writeln!(
out,
"{:<name_width$} {:<email_width$} {:<scope_width$} {}",
names[i],
email_strings[i],
scope_strings[i],
if account.is_default { "*" } else { "" },
)
.context("Failed to write account row")?;
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
fn sample(name: &str, is_default: bool) -> AccountSummary {
AccountSummary {
name: name.to_string(),
email_address: Some(format!("{name}@example.com")),
scope: Some("readonly".to_string()),
is_default,
}
}
#[test]
fn render_table_empty_points_at_login() {
let mut buf = Vec::new();
render_account_table(&[], &mut buf).unwrap();
let out = String::from_utf8(buf).unwrap();
assert!(out.contains("drive auth login --account"));
}
#[test]
fn render_table_writes_header_and_marks_default() {
let accounts = [sample("work", true), sample("personal", false)];
let mut buf = Vec::new();
render_account_table(&accounts, &mut buf).unwrap();
let out = String::from_utf8(buf).unwrap();
assert!(out.contains("NAME"));
assert!(out.contains("EMAIL"));
assert!(out.contains("SCOPE"));
assert!(out.contains("DEFAULT"));
assert!(out.contains("work"));
assert!(out.contains("personal"));
let work_line = out
.lines()
.find(|l| l.contains("work@example.com"))
.unwrap();
assert!(work_line.trim_end().ends_with('*'));
let personal_line = out
.lines()
.find(|l| l.contains("personal@example.com"))
.unwrap();
assert!(!personal_line.trim_end().ends_with('*'));
}
#[test]
fn render_table_strips_control_bytes_and_keeps_columns_aligned() {
let accounts = [
AccountSummary {
name: "wo\rrk".to_string(),
email_address: Some("evil\x1b[31m@example.com".to_string()),
scope: Some("read\x07only".to_string()),
is_default: true,
},
sample("personal", false),
];
let mut buf = Vec::new();
render_account_table(&accounts, &mut buf).unwrap();
let out = String::from_utf8(buf).unwrap();
assert!(
!out.contains(|c: char| c.is_control() && c != '\n'),
"{out:?}"
);
assert!(out.contains("evil[31m@example.com"), "{out:?}");
let lines: Vec<&str> = out.lines().collect();
assert_eq!(lines.len(), 3);
assert_eq!(
lines[1].find("readonly"),
lines[2].find("readonly"),
"{out:?}"
);
}
#[test]
fn render_table_uses_dash_for_missing_email_and_scope() {
let account = AccountSummary {
name: "bare".to_string(),
email_address: None,
scope: None,
is_default: false,
};
let mut buf = Vec::new();
render_account_table(&[account], &mut buf).unwrap();
let out = String::from_utf8(buf).unwrap();
assert!(out.contains('-'));
}
#[test]
fn run_list_table_path_writes_to_stdout() {
run_list(&[sample("work", true)], &OutputFormat::Table).unwrap();
}
#[test]
fn run_list_json_path_returns_ok() {
run_list(&[sample("work", true)], &OutputFormat::Json).unwrap();
}
}