use crate::output::terminal_text;
use anyhow::{Result, bail};
use colored::Colorize;
use crate::hosts;
fn format_alias_rows(list: &[(String, hosts::HostEntry)]) -> String {
if list.is_empty() {
return format!(
"No saved aliases. Use `denki alias <name> <ip> [--klap | --tapo]` to add one.\nFile: {}",
terminal_text(&hosts::path_display())
);
}
let mut out = String::new();
out.push_str(&format!(
"{:<30} {:<18} {}\n",
"Name".bold(),
"IP".bold(),
"Protocol".bold()
));
out.push_str(&format!("{}\n", "─".repeat(58).dimmed()));
for (name, entry) in list {
out.push_str(&format!(
"{:<30} {:<18} {}\n",
terminal_text(name),
terminal_text(&entry.ip),
format_args!(
"{}{}{}",
entry.protocol,
if entry.protocol == hosts::Protocol::Kasa && !entry.group_approved {
" (group approval required)"
} else {
""
},
if entry.physically_confirmed {
" (physically confirmed by user)"
} else {
" (not physically confirmed)"
}
)
));
}
out.push_str(&format!(
"({} aliases in {})",
list.len(),
terminal_text(&hosts::path_display())
));
out
}
pub fn handle_alias(name: &str, ip: &str, klap: bool, tapo: bool) -> Result<()> {
if klap && tapo {
bail!("Choose either --klap or --tapo");
}
let protocol = if tapo {
hosts::Protocol::Tapo
} else if klap {
hosts::Protocol::Klap
} else {
hosts::Protocol::Kasa
};
hosts::set(name, ip, protocol.clone())?;
crate::output::record(serde_json::json!({"alias":name,"ip":ip,"protocol":protocol}));
let tag = if tapo {
" (tapo: auto TPAP/KLAP)".dimmed()
} else if klap {
" (klap)".dimmed()
} else {
"".normal()
};
crate::output::println!(
"Saved: {} → {}{}",
terminal_text(name).bold(),
terminal_text(ip),
tag
);
Ok(())
}
pub fn handle_unalias(name: &str) -> Result<()> {
if hosts::remove(name)? {
crate::output::record(serde_json::json!({"removed":name}));
crate::output::println!("Removed alias \"{}\"", terminal_text(name));
} else {
bail!("No alias named \"{name}\" found");
}
Ok(())
}
pub fn handle_aliases() -> Result<()> {
let list = hosts::list()?;
crate::output::record(
serde_json::json!({"aliases":list.iter().map(|(alias,e)| serde_json::json!({"alias":alias,"ip":e.ip,"protocol":e.protocol,"identity_known":e.device_id.is_some(),"physically_confirmed":e.physically_confirmed,"group_approved":e.protocol != hosts::Protocol::Kasa || e.group_approved})).collect::<Vec<_>>()}),
);
crate::output::println!("{}", format_alias_rows(&list));
Ok(())
}
pub fn handle_identify(name: &str, confirmed: bool) -> Result<()> {
hosts::identify(name, confirmed)?;
crate::output::record(serde_json::json!({"alias":name,"physically_confirmed":confirmed}));
crate::output::println!(
"{}: {}",
terminal_text(name),
if confirmed {
"physical identification recorded from your confirmation"
} else {
"physical identification cleared"
}
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(ip: &str, protocol: hosts::Protocol) -> hosts::HostEntry {
hosts::HostEntry {
ip: ip.to_string(),
protocol,
device_id: None,
group_approved: false,
physically_confirmed: false,
}
}
#[test]
fn format_alias_rows_handles_empty_list() {
let rendered = format_alias_rows(&[]);
assert!(rendered.contains("No saved aliases"), "{rendered}");
assert!(rendered.contains("denki alias"), "{rendered}");
}
#[test]
fn format_alias_rows_includes_table_content() {
let rendered = format_alias_rows(&[(
"Lamp".to_string(),
entry("192.168.1.10", hosts::Protocol::Kasa),
)]);
assert!(rendered.contains("Lamp"), "{rendered}");
assert!(rendered.contains("192.168.1.10"), "{rendered}");
assert!(rendered.contains("kasa"), "{rendered}");
}
}