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}