use clap::Parser;
use four_word_networking::{FourWordAdaptiveEncoder, FourWordError, Result};
use std::io::{self, Write};
use std::process;
use crossterm::{
cursor,
event::{self, Event, KeyCode, KeyEventKind},
execute, queue,
style::{Color, Print, ResetColor, SetForegroundColor},
terminal::{self, ClearType},
};
#[derive(Parser)]
#[command(
name = "4wn",
about = "Four-Word Networking - Interactive IP address to words converter",
long_about = "Interactive CLI for converting between IP addresses and memorable words.\n\
Default: Interactive mode with autocomplete hints and progressive completion.\n\
With arguments: Direct conversion between IP addresses and four-word combinations.\n\
Features perfect reconstruction for IPv4 (4 words) and adaptive compression for IPv6 (6/9/12 words).",
version
)]
struct Cli {
input: Vec<String>,
#[arg(short, long)]
verbose: bool,
#[arg(short, long)]
quiet: bool,
#[arg(short, long)]
complete: Option<String>,
#[arg(long)]
validate: Option<String>,
}
fn main() {
let cli = Cli::parse();
if let Err(e) = run(cli) {
eprintln!("Error: {e}");
process::exit(1);
}
}
fn run(cli: Cli) -> Result<()> {
let encoder = FourWordAdaptiveEncoder::new()?;
if let Some(prefix) = cli.complete {
show_completion_hints(&encoder, &prefix)?;
return Ok(());
}
if let Some(partial) = cli.validate {
show_validation_results(&encoder, &partial)?;
return Ok(());
}
if cli.input.is_empty() {
return interactive_mode(&encoder, cli.verbose);
}
let input = if cli.input.len() == 1 {
cli.input[0].trim().to_string()
} else {
cli.input.join(" ")
};
if looks_like_words(&input) {
decode_words(&encoder, &input, cli.verbose, cli.quiet)
} else {
encode_address(&encoder, &input, cli.verbose, cli.quiet)
}
}
fn looks_like_words(input: &str) -> bool {
let segments: Vec<&str> = if input.contains(' ') && !input.contains('-') && !input.contains(':')
{
input.split_whitespace().collect()
} else if input.contains('.')
|| input.contains('-')
|| input.contains('_')
|| input.contains('+')
{
input.split(|c: char| ".-_+".contains(c)).collect()
} else {
return false;
};
if segments.len() != 4 && segments.len() != 6 && segments.len() != 9 && segments.len() != 12 {
return false;
}
segments
.iter()
.all(|segment| !segment.is_empty() && segment.chars().all(|c| c.is_alphabetic()))
}
fn encode_address(
encoder: &FourWordAdaptiveEncoder,
address: &str,
verbose: bool,
quiet: bool,
) -> Result<()> {
let words = encoder.encode(address)?;
if quiet {
println!("{words}");
} else if verbose {
println!("Input: {address}");
println!("Words: {words}");
println!("Encoding: Perfect (100% reversible)");
if words.contains('.') && !words.contains('-') {
println!("Type: IPv4 (dot separators, lowercase)");
} else if words.contains('-') {
println!("Type: IPv6 (dash separators, title case)");
}
println!("Features:");
println!(" • Perfect IPv4 reconstruction (4 words)");
println!(" • Adaptive IPv6 compression (6, 9, or 12 words)");
println!(" • Guaranteed perfect reconstruction");
} else {
println!("{words}");
}
Ok(())
}
fn decode_words(
encoder: &FourWordAdaptiveEncoder,
words: &str,
verbose: bool,
quiet: bool,
) -> Result<()> {
let address = encoder.decode(words)?;
if quiet {
println!("{address}");
} else if verbose {
println!("Input: {words}");
println!("Address: {address}");
println!("Decoding: Perfect reconstruction");
if words.contains('.') && !words.contains('-') {
println!("Type: IPv4 (detected from dot separators)");
} else if words.contains('-') {
println!("Type: IPv6 (detected from dash separators)");
}
} else {
println!("{address}");
}
Ok(())
}
fn interactive_mode(encoder: &FourWordAdaptiveEncoder, verbose: bool) -> Result<()> {
terminal::enable_raw_mode()
.map_err(|e| FourWordError::InvalidInput(format!("Failed to enable raw mode: {e}")))?;
let cleanup = || {
let _ = terminal::disable_raw_mode();
let _ = execute!(io::stdout(), cursor::Show, ResetColor);
};
let original_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let _ = terminal::disable_raw_mode();
let _ = execute!(io::stdout(), cursor::Show, ResetColor);
original_hook(info);
}));
let result = run_interactive_tui(encoder, verbose);
cleanup();
result
}
fn run_interactive_tui(encoder: &FourWordAdaptiveEncoder, verbose: bool) -> Result<()> {
let mut stdout = io::stdout();
execute!(
stdout,
terminal::Clear(ClearType::All),
cursor::MoveTo(0, 0),
Print(
"🌐 Four-Word Networking - Interactive Mode
"
),
Print(
"Real-time autocomplete: Progressive hints at 3+ chars, auto-complete at 5 chars
"
),
Print(
"Commands: quit/exit to leave, Ctrl+C to interrupt, Tab for completion
"
)
)
.map_err(|e| FourWordError::InvalidInput(format!("Terminal error: {e}")))?;
let mut current_input = String::new();
let mut cursor_pos = 0;
let mut completed_words = Vec::<String>::new();
loop {
render_ui(
&mut stdout,
¤t_input,
cursor_pos,
&completed_words,
encoder,
verbose,
)?;
if let Ok(event) = event::read() {
match event {
Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
match key_event.code {
KeyCode::Char('c')
if key_event.modifiers.contains(event::KeyModifiers::CONTROL) =>
{
break;
}
KeyCode::Char('q')
if key_event.modifiers.contains(event::KeyModifiers::CONTROL) =>
{
break;
}
KeyCode::Enter => {
if handle_enter(¤t_input, &mut completed_words, encoder, verbose)?
{
break;
}
current_input.clear();
cursor_pos = 0;
}
KeyCode::Tab => {
if let Some(completion) = get_best_completion(encoder, ¤t_input) {
current_input = completion;
cursor_pos = current_input.len();
}
}
KeyCode::Backspace => {
if cursor_pos > 0 {
current_input.remove(cursor_pos - 1);
cursor_pos -= 1;
}
}
KeyCode::Delete => {
if cursor_pos < current_input.len() {
current_input.remove(cursor_pos);
}
}
KeyCode::Left => {
cursor_pos = cursor_pos.saturating_sub(1);
}
KeyCode::Right => {
if cursor_pos < current_input.len() {
cursor_pos += 1;
}
}
KeyCode::Home => {
cursor_pos = 0;
}
KeyCode::End => {
cursor_pos = current_input.len();
}
KeyCode::Char(c) => {
current_input.insert(cursor_pos, c);
cursor_pos += 1;
if current_input.len() >= 5
&& let Some(word) = encoder.auto_complete_at_five(¤t_input)
{
current_input = word;
cursor_pos = current_input.len();
}
}
_ => {}
}
}
_ => {}
}
}
}
execute!(
stdout,
terminal::Clear(ClearType::All),
cursor::MoveTo(0, 0),
Print(
"Goodbye! 👋
"
)
)
.map_err(|e| FourWordError::InvalidInput(format!("Terminal error: {e}")))?;
Ok(())
}
fn render_ui(
stdout: &mut io::Stdout,
current_input: &str,
cursor_pos: usize,
completed_words: &[String],
encoder: &FourWordAdaptiveEncoder,
_verbose: bool,
) -> Result<()> {
queue!(stdout, cursor::MoveTo(0, 4))?;
queue!(stdout, terminal::Clear(ClearType::FromCursorDown))?;
if !completed_words.is_empty() {
queue!(
stdout,
SetForegroundColor(Color::Green),
Print("Words: "),
ResetColor,
Print(&completed_words.join(" ")),
Print(&format!(" ({}/4)", completed_words.len())),
Print(
"
"
)
)?;
}
queue!(stdout, Print("4wn> "), Print(current_input))?;
if current_input.len() >= 3 {
let hints = encoder.get_word_hints(current_input);
if !hints.is_empty() {
queue!(
stdout,
Print(
"
"
)
)?;
if hints.len() == 1 {
queue!(
stdout,
SetForegroundColor(Color::Green),
Print("✓ Complete match: "),
ResetColor,
Print(&hints[0])
)?;
} else {
queue!(
stdout,
SetForegroundColor(Color::Yellow),
Print(&format!("💡 {} matches: ", hints.len())),
ResetColor
)?;
for (i, hint) in hints.iter().take(5).enumerate() {
if i > 0 {
queue!(stdout, Print(", "))?;
}
queue!(stdout, Print(hint))?;
}
if hints.len() > 5 {
queue!(stdout, Print(&format!(" (+{} more)", hints.len() - 5)))?;
}
}
if current_input.len() >= 5 && hints.len() == 1 {
queue!(
stdout,
Print(
"
"
),
SetForegroundColor(Color::Cyan),
Print(" Press any key to auto-complete"),
ResetColor
)?;
}
} else {
queue!(
stdout,
Print(
"
"
),
SetForegroundColor(Color::Red),
Print("❌ No matches found"),
ResetColor
)?;
}
}
let cursor_col = 5 + cursor_pos; queue!(
stdout,
cursor::MoveTo(
cursor_col as u16,
4 + if completed_words.is_empty() { 0 } else { 2 }
)
)?;
stdout
.flush()
.map_err(|e| FourWordError::InvalidInput(format!("Flush error: {e}")))?;
Ok(())
}
fn handle_enter(
input: &str,
completed_words: &mut Vec<String>,
encoder: &FourWordAdaptiveEncoder,
_verbose: bool,
) -> Result<bool> {
let input = input.trim();
match input.to_lowercase().as_str() {
"quit" | "exit" => return Ok(true),
"clear" => {
completed_words.clear();
return Ok(false);
}
"" => return Ok(false),
_ => {}
}
if (input.contains(':') || input.contains('[') || input.parse::<std::net::IpAddr>().is_ok())
&& let Ok(encoded) = encoder.encode(input)
{
println!("\n🌐 {input} → {encoded}\n");
return Ok(false);
}
if looks_like_words(input)
&& let Ok(decoded) = encoder.decode(input)
{
println!("\n🌐 {input} → {decoded}\n");
return Ok(false);
}
if encoder.is_valid_prefix(input) {
let hints = encoder.get_word_hints(input);
if hints.len() == 1 {
completed_words.push(hints[0].clone());
if completed_words.len() == 4 {
let word_str = completed_words.join(" ");
match encoder.decode(&word_str) {
Ok(decoded) => {
println!(
"
✅ Complete! {word_str} → {decoded}
"
);
completed_words.clear();
}
Err(_) => {
println!(
"
❌ Invalid word combination
"
);
completed_words.clear();
}
}
}
} else {
println!(
"
❌ Ambiguous input: {} matches found
",
hints.len()
);
}
} else {
println!(
"
❌ Invalid word or prefix
"
);
}
Ok(false)
}
fn get_best_completion(encoder: &FourWordAdaptiveEncoder, input: &str) -> Option<String> {
if input.len() >= 3 {
let hints = encoder.get_word_hints(input);
if hints.len() == 1 {
Some(hints[0].clone())
} else if !hints.is_empty() {
hints.into_iter().min_by_key(|s| s.len())
} else {
None
}
} else {
None
}
}
#[allow(dead_code)]
fn process_complete_input(
encoder: &FourWordAdaptiveEncoder,
input: &str,
verbose: bool,
) -> Result<Option<String>> {
if input.contains(':') || input.contains('[') || input.parse::<std::net::IpAddr>().is_ok() {
let encoded = encoder.encode(input)?;
if verbose {
Ok(Some(format!("{input} → {encoded}")))
} else {
Ok(Some(encoded))
}
} else if looks_like_words(input) {
let decoded = encoder.decode(input)?;
if verbose {
Ok(Some(format!("{input} → {decoded}")))
} else {
Ok(Some(decoded))
}
} else {
Err(FourWordError::InvalidInput(
"Not complete input".to_string(),
))
}
}
#[allow(dead_code)]
fn handle_progressive_input(
encoder: &FourWordAdaptiveEncoder,
input: &str,
current_input: &mut String,
current_words: &mut Vec<String>,
) {
current_input.push_str(input);
if current_input.contains(' ') {
let parts: Vec<&str> = current_input.split(' ').collect();
if let Some(word) = parts.first().filter(|w| !w.is_empty()) {
if let Some(completed) = try_complete_word(encoder, word) {
current_words.push(completed);
*current_input = parts[1..].join(" ");
if current_words.len() == 4 {
let word_sequence = current_words.join(" ");
if let Ok(decoded) = encoder.decode(&word_sequence) {
println!("✅ Complete! {word_sequence} → {decoded}");
}
current_words.clear();
current_input.clear();
}
} else {
println!("❌ '{word}' is not a valid word. Try again.");
current_input.clear();
}
}
} else {
show_progressive_hints(encoder, current_input);
}
}
#[allow(dead_code)]
fn try_complete_word(encoder: &FourWordAdaptiveEncoder, partial: &str) -> Option<String> {
if partial.len() >= 5 {
return encoder.auto_complete_at_five(partial);
}
let hints = encoder.get_word_hints(partial);
if hints.len() == 1 && hints[0] == partial {
return Some(partial.to_string());
}
None
}
#[allow(dead_code)]
fn show_progressive_hints(encoder: &FourWordAdaptiveEncoder, input: &str) {
if input.len() < 3 {
return;
}
let hints = encoder.get_word_hints(input);
match hints.len() {
0 => println!("❌ No words start with '{input}'"),
1 => {
if hints[0] == input {
println!("✅ '{input}' is complete");
} else {
println!("💡 Complete: {}", hints[0]);
}
}
2..=5 => {
println!("💡 Hints: {}", hints.join(", "));
}
_ => {
println!(
"💡 {} possibilities: {}, ...",
hints.len(),
hints[..3].join(", ")
);
}
}
}
fn show_completion_hints(encoder: &FourWordAdaptiveEncoder, prefix: &str) -> Result<()> {
let hints = encoder.get_word_hints(prefix);
if hints.is_empty() {
println!("No words found starting with '{prefix}'");
} else {
println!("Completions for '{}' ({} found):", prefix, hints.len());
for hint in hints.iter().take(10) {
println!(" {hint}");
}
if hints.len() > 10 {
println!(" ... and {} more", hints.len() - 10);
}
}
Ok(())
}
fn show_validation_results(encoder: &FourWordAdaptiveEncoder, partial: &str) -> Result<()> {
let result = encoder.validate_partial_input(partial)?;
println!("Validation results for '{partial}':");
println!(" Valid prefix: {}", result.is_valid_prefix);
println!(" Words so far: {}", result.word_count_so_far);
println!(" Is complete: {}", result.is_complete);
if !result.possible_completions.is_empty() {
println!(" Completions: {}", result.possible_completions.join(", "));
}
Ok(())
}
#[allow(dead_code)]
fn show_help() {
println!("🌐 Four-Word Networking Help");
println!();
println!("Interactive Mode Commands:");
println!(" help - Show this help");
println!(" clear - Clear current input");
println!(" quit - Exit the program");
println!();
println!("Usage:");
println!(" • Type IP address → get 4 words (IPv4) or 6/9/12 words (IPv6)");
println!(" • Type words → get IP address");
println!(" • Progressive hints appear at 3+ characters");
println!(" • Auto-completion happens at 5+ characters");
println!(" • Space completes current word and moves to next");
println!();
println!("Examples:");
println!(" 192.168.1.1 → four memorable words");
println!(" about beam cat → reconstructed IP address");
println!(" ::1 → six words for IPv6 loopback");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_looks_like_words() {
assert!(looks_like_words("ocean thunder falcon star"));
assert!(looks_like_words("ocean thunder falcon star book april"));
assert!(looks_like_words(
"ocean thunder falcon star book april wing moon river"
));
assert!(looks_like_words("ocean.thunder.falcon.star"));
assert!(!looks_like_words("ocean.thunder.falcon"));
assert!(!looks_like_words("a.b.c.d.e"));
assert!(!looks_like_words("ocean.thunder.123"));
assert!(!looks_like_words("192.168.1.1"));
assert!(!looks_like_words("ocean:thunder:falcon"));
}
}