digit-cli 0.3.0

A finger protocol client (RFC 1288 / RFC 742)
Documentation
use std::io::Write as _;
use std::process::ExitCode;
use std::time::Duration;

use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::aot::generate;

use digit::protocol::{finger, finger_raw};
use digit::query::{Query, DEFAULT_PORT};

#[derive(Subcommand, Debug)]
enum Command {
    /// Generate shell completions for the given shell
    Completions {
        /// Shell to generate completions for (bash, zsh, fish, powershell, elvish)
        shell: clap_complete::Shell,
    },
}

/// digit - a finger protocol client (RFC 1288 / RFC 742)
///
/// Query user information from finger servers. Supports standard queries,
/// user listings, and forwarding chains.
///
/// Note: Forwarding queries (user@host1@host2) depend on server support.
/// Many modern finger servers disable forwarding for security reasons.
#[derive(Parser, Debug)]
#[command(version, about)]
struct Cli {
    #[command(subcommand)]
    command: Option<Command>,

    /// Finger query (e.g. "user@host", "@host", "user@host1@host2")
    query: Option<String>,

    /// Request verbose/long output (sends /W)
    #[arg(short, long)]
    long: bool,

    /// Port to connect on
    #[arg(short, long, default_value_t = DEFAULT_PORT)]
    port: u16,

    /// Connection timeout in seconds
    #[arg(short, long, default_value_t = 10)]
    timeout: u64,

    /// Maximum response size in bytes
    #[arg(long, default_value_t = 1_048_576)]
    max_size: u64,

    /// Write raw response bytes to stdout without UTF-8 decoding
    #[arg(long)]
    raw: bool,
}

fn main() -> ExitCode {
    let cli = Cli::parse();

    if let Some(Command::Completions { shell }) = cli.command {
        let mut cmd = Cli::command();
        generate(shell, &mut cmd, "digit", &mut std::io::stdout());
        return ExitCode::SUCCESS;
    }

    let query = match Query::parse(cli.query.as_deref(), cli.long, cli.port) {
        Ok(q) => q,
        Err(e) => {
            eprintln!("digit: {}", e);
            return ExitCode::FAILURE;
        }
    };
    let timeout = Duration::from_secs(cli.timeout);

    if cli.raw {
        match finger_raw(&query, timeout, cli.max_size) {
            Ok(bytes) => {
                if bytes.len() as u64 >= cli.max_size {
                    eprintln!(
                        "digit: warning: response truncated at {} bytes (use --max-size to increase)",
                        cli.max_size
                    );
                }
                std::io::stdout()
                    .write_all(&bytes)
                    .expect("failed to write to stdout");
                ExitCode::SUCCESS
            }
            Err(e) => {
                eprintln!("digit: {}", e);
                ExitCode::FAILURE
            }
        }
    } else {
        match finger(&query, timeout, cli.max_size) {
            Ok(response) => {
                if response.len() as u64 >= cli.max_size {
                    eprintln!(
                        "digit: warning: response truncated at {} bytes (use --max-size to increase)",
                        cli.max_size
                    );
                }
                print!("{}", response);
                ExitCode::SUCCESS
            }
            Err(e) => {
                eprintln!("digit: {}", e);
                ExitCode::FAILURE
            }
        }
    }
}