mod client;
mod error;
mod room;
mod server;
mod tui;
mod tunnel;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
const BANNER: &str = r#"
_____ _____ _____ ____ _ _
/ ____|/ ____| __ \ / __ \| | | |
| (___ | | | |__) | | | | | | |
\___ \| | | _ /| | | | | | |
____) | |____| | \ \| |__| | |____| |____
|_____/ \_____|_| \_\\____/|______|______|
Secure SSH Chat - Because privacy matters 🔐
"#;
#[derive(Parser, Debug)]
#[command(
name = "scroll",
version,
about = "A lightweight Rust-based CLI tool for secure, real-time communication over SSH",
long_about = BANNER
)]
struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(short, long, global = true)]
verbose: bool,
}
#[derive(Subcommand, Debug)]
enum Commands {
Create {
#[arg(short, long, default_value = "2222")]
port: u16,
#[arg(long, short = 'w')]
pw: String,
#[arg(short, long, default_value = "host")]
user: String,
#[arg(short, long)]
tunnel: bool,
#[arg(short, long, default_value = "Scroll Chat")]
name: String,
},
Join {
address: String,
#[arg(long, short = 'w')]
pw: String,
#[arg(short, long)]
user: String,
},
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
let filter = if cli.verbose {
EnvFilter::new("scroll=debug,russh=debug")
} else {
EnvFilter::new("scroll=info")
};
tracing_subscriber::registry()
.with(fmt::layer().with_target(false).with_level(true))
.with(filter)
.init();
match cli.command {
Commands::Create { port, pw, user, tunnel, name } => {
create_room(port, pw, user, tunnel, name).await
}
Commands::Join { address, pw, user } => {
join_room(address, pw, user).await
}
}
}
async fn create_room(
port: u16,
password: String,
_username: String, enable_tunnel: bool,
room_name: String,
) -> Result<()> {
eprintln!("{}", BANNER);
let password_hash = room::ChatRoom::hash_password(&password)
.context("Failed to hash password")?;
let room = room::ChatRoom::new(room_name.clone(), password_hash);
eprintln!("📜 Room: {}", room_name);
eprintln!("🔒 Password protected: Yes");
eprintln!("🌐 Local: scroll join 127.0.0.1:{} --pw <password> --user <name>", port);
let _tunnel = if enable_tunnel {
eprintln!("\n🌍 Starting bore tunnel (free, no signup needed)...");
match tunnel::BoreTunnel::start(port).await {
Ok(t) => {
let connection = t.connection_string();
room.set_tunnel_url(connection.clone()).await;
eprintln!("🔗 Public: scroll join {} --pw <password> --user <name>", connection);
eprintln!(" Share this with others to join!");
Some(t)
}
Err(e) => {
eprintln!("⚠️ Tunnel failed: {}", e);
eprintln!(" Continuing with local-only access...");
None
}
}
} else {
None
};
eprintln!("\n✨ Room is ready! Waiting for connections...");
eprintln!(" Press Ctrl+C to close the room.\n");
tokio::spawn(async move {
tokio::signal::ctrl_c().await.ok();
eprintln!("\n\n👋 Shutting down room...");
});
let server = server::ChatServer::new(room);
server.run(port).await?;
Ok(())
}
async fn join_room(address: String, password: String, username: String) -> Result<()> {
eprintln!("{}", BANNER);
let config = client::ClientConfig {
address,
password,
username,
};
client::connect(config).await
}