Skip to main content

ferry_cli/
lib.rs

1use std::net::SocketAddr;
2use std::path::PathBuf;
3
4use clap::{Args, CommandFactory, Parser, Subcommand};
5
6#[derive(Debug, Parser)]
7#[command(
8    name = "ferry",
9    version,
10    about = "Terminal-native LAN file transfer",
11    long_about = "FileFerry moves files and directories between local-network ferry peers over QUIC, with mDNS discovery, fingerprint pinning, daemon receive mode, a TUI send flow, and newline-delimited JSON for scripts.",
12    after_help = "Examples:\n  ferry recv --listen 0.0.0.0:53318 --dest ~/Downloads/ferry\n  ferry send stephen-mbp ./bundle\n  ferry send --fingerprint <full-fingerprint> 192.168.1.42:53318 ./bundle\n  ferry --json send 192.168.1.42:53318 ./bundle\n  ferry peers trust <alias-or-full-fingerprint>\n  ferry daemon --listen 0.0.0.0:53318 --dest ~/Downloads/ferry"
13)]
14pub struct Cli {
15    #[arg(
16        long,
17        global = true,
18        help = "Override the default command port where supported"
19    )]
20    pub port: Option<u16>,
21
22    #[arg(
23        long,
24        global = true,
25        help = "Bind command listeners to a specific interface where supported"
26    )]
27    pub bind: Option<String>,
28
29    #[arg(long, global = true, help = "Emit newline-delimited JSON on stdout")]
30    pub json: bool,
31
32    #[arg(long, global = true, help = "Disable mDNS discovery and announcements")]
33    pub no_discovery: bool,
34
35    #[arg(short, long, global = true, help = "Reduce human output")]
36    pub quiet: bool,
37
38    #[arg(short, long, action = clap::ArgAction::Count, global = true, help = "Increase diagnostic output")]
39    pub verbose: u8,
40
41    #[command(subcommand)]
42    pub command: Option<Command>,
43}
44
45impl Cli {
46    pub fn clap_command() -> clap::Command {
47        Self::command()
48    }
49}
50
51#[derive(Debug, Subcommand)]
52pub enum Command {
53    #[command(
54        about = "Send files or directories to a ferry peer",
55        after_help = "Examples:\n  ferry send stephen-mbp ./bundle\n  ferry send 192.168.1.42:53318 ./bundle\n  ferry send --fingerprint <full-fingerprint> 192.168.1.42:53318 ./bundle\n  ferry send --psk-file ~/.config/ferry/transfer.psk 192.168.1.42:53318 ./bundle"
56    )]
57    Send(SendArgs),
58    #[command(
59        about = "Receive one transfer in the foreground",
60        after_help = "Examples:\n  ferry recv --listen 0.0.0.0:53318 --dest ~/Downloads/ferry\n  ferry recv --no-discovery --listen 127.0.0.1:53318 --dest ./incoming\n  ferry recv --psk-file ~/.config/ferry/transfer.psk --listen 0.0.0.0:53318"
61    )]
62    Recv(RecvArgs),
63    #[command(about = "List and manage trusted or discovered peers")]
64    Peers {
65        #[command(subcommand)]
66        command: Option<PeersCommand>,
67    },
68    #[command(
69        about = "Run a long-lived receiver using persisted daemon settings",
70        after_help = "Examples:\n  ferry daemon\n  ferry daemon --listen 0.0.0.0:53318 --dest ~/Downloads/ferry\n  ferry daemon --psk-file ~/.config/ferry/transfer.psk"
71    )]
72    Daemon(DaemonArgs),
73    #[command(about = "Print the redacted configuration")]
74    Config,
75    #[command(about = "Print the local alias and device fingerprint")]
76    Identity,
77    #[command(about = "Print the ferry version")]
78    Version,
79    #[command(about = "Open the interactive terminal send interface")]
80    Tui,
81}
82
83#[derive(Debug, Args)]
84pub struct SendArgs {
85    #[arg(
86        long,
87        value_name = "FINGERPRINT",
88        help = "Require the receiver certificate to match this full fingerprint"
89    )]
90    pub fingerprint: Option<String>,
91
92    #[arg(
93        long,
94        value_name = "FILE",
95        help = "Read an explicit transfer PSK from a file"
96    )]
97    pub psk_file: Option<PathBuf>,
98
99    #[arg(help = "Peer alias, hostname, fingerprint prefix, or direct ip:port")]
100    pub peer: String,
101    #[arg(value_name = "PATH", help = "File or directory to send")]
102    pub paths: Vec<PathBuf>,
103}
104
105#[derive(Debug, Args)]
106pub struct RecvArgs {
107    #[arg(long, help = "Listen address for incoming QUIC transfers")]
108    pub listen: Option<SocketAddr>,
109
110    #[arg(long, value_name = "DIR", help = "Destination directory")]
111    pub dest: Option<PathBuf>,
112
113    #[arg(
114        long,
115        help = "Accept the incoming transfer without interactive confirmation"
116    )]
117    pub accept_all: bool,
118
119    #[arg(
120        long,
121        value_name = "FILE",
122        help = "Require an explicit transfer PSK read from a file"
123    )]
124    pub psk_file: Option<PathBuf>,
125}
126
127#[derive(Debug, Args)]
128pub struct DaemonArgs {
129    #[arg(long, help = "Listen address for incoming QUIC transfers")]
130    pub listen: Option<SocketAddr>,
131
132    #[arg(long, value_name = "DIR", help = "Destination directory")]
133    pub dest: Option<PathBuf>,
134
135    #[arg(
136        long,
137        value_name = "FILE",
138        help = "Require an explicit transfer PSK read from a file"
139    )]
140    pub psk_file: Option<PathBuf>,
141}
142
143#[derive(Debug, Subcommand)]
144pub enum PeersCommand {
145    #[command(
146        about = "Trust a full fingerprint or discovered peer hint",
147        after_help = "Examples:\n  ferry peers trust 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\n  ferry peers trust stephen-mbp\n  ferry peers trust 012345"
148    )]
149    Trust {
150        #[arg(value_name = "FINGERPRINT_OR_HINT")]
151        fingerprint: String,
152    },
153    #[command(about = "Remove a trusted peer fingerprint")]
154    Forget {
155        #[arg(value_name = "FINGERPRINT")]
156        fingerprint: String,
157    },
158}