feat(cli): rooms and chat subcommands

Add `rooms` (list public rooms with user counts, busiest first) and
`chat <room> [message]` (join a room; say a message and exit, or listen
and print incoming messages/joins/leaves for a while). Factor a shared
connect_and_login helper.
This commit is contained in:
michel
2026-07-19 19:49:17 +02:00
parent 7bc0ee2c3d
commit 7d94b2caa3
2 changed files with 139 additions and 0 deletions
+16
View File
@@ -104,6 +104,22 @@ pub enum Commands {
/// Username whose shares to list
username: String,
},
/// List the public chat rooms and their user counts
Rooms,
/// Join a chat room: print messages, or send one and exit
Chat {
/// Name of the room to join
room: String,
/// Optional message to say in the room (omit to just listen)
message: Option<String>,
/// Seconds to stay and print incoming messages when only listening
#[arg(short, long, default_value = "30")]
listen_secs: u64,
},
}
pub fn parse_server_address(server: &str) -> color_eyre::Result<(String, u16)> {
+123
View File
@@ -115,6 +115,31 @@ fn main() -> Result<()> {
},
&target,
),
Some(Commands::Rooms) => list_rooms(&ClientSettings {
username,
password,
server_address: PeerAddress::new(server_host, server_port),
enable_listen: !cli.disable_listener,
listen_port: cli.listener_port,
shared_directory,
}),
Some(Commands::Chat {
room,
message,
listen_secs,
}) => chat_room(
&ClientSettings {
username,
password,
server_address: PeerAddress::new(server_host, server_port),
enable_listen: !cli.disable_listener,
listen_port: cli.listener_port,
shared_directory,
},
&room,
message.as_deref(),
listen_secs,
),
None => {
use ratatui::crossterm::{
event::EnableMouseCapture,
@@ -205,6 +230,104 @@ fn browse_user(settings: &ClientSettings, target: &str) -> Result<()> {
))
}
/// Connect and log in, returning the ready client or a descriptive error.
fn connect_and_login(settings: &ClientSettings) -> Result<Client> {
let mut client = Client::with_settings(settings.clone());
client
.connect()
.map_err(|e| color_eyre::eyre::eyre!("Failed to connect: {}", e))?;
if !client
.login()
.map_err(|e| color_eyre::eyre::eyre!("Failed to login: {}", e))?
{
return Err(color_eyre::eyre::eyre!("Login rejected by server"));
}
Ok(client)
}
fn list_rooms(settings: &ClientSettings) -> Result<()> {
use std::time::Instant;
let client = connect_and_login(settings)?;
client
.request_room_list()
.map_err(|e| color_eyre::eyre::eyre!("Failed to list rooms: {}", e))?;
println!("📋 Fetching room list...");
let deadline = Instant::now() + Duration::from_secs(5);
let mut rooms = client.room_list();
while rooms.is_empty() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(200));
rooms = client.room_list();
}
rooms.sort_by(|a, b| {
b.user_count
.cmp(&a.user_count)
.then_with(|| a.name.cmp(&b.name))
});
if rooms.is_empty() {
println!("(no public rooms reported)");
} else {
println!("{:>6} room", "users");
for room in rooms {
println!("{:>6} {}", room.user_count, room.name);
}
}
Ok(())
}
fn chat_room(
settings: &ClientSettings,
room: &str,
message: Option<&str>,
listen_secs: u64,
) -> Result<()> {
use soulseek_rs::types::RoomEvent;
use std::time::Instant;
let client = connect_and_login(settings)?;
client
.join_room(room)
.map_err(|e| color_eyre::eyre::eyre!("Failed to join room: {}", e))?;
if let Some(text) = message {
client
.say_in_room(room, text)
.map_err(|e| color_eyre::eyre::eyre!("Failed to say: {}", e))?;
// Let the server actor flush before we drop the client.
std::thread::sleep(Duration::from_millis(500));
println!("💬 {room}: {text}");
return Ok(());
}
println!(
"💬 Joined {room}. Listening for {listen_secs}s (Ctrl-C to quit)..."
);
let deadline = Instant::now() + Duration::from_secs(listen_secs);
while Instant::now() < deadline {
for event in client.take_room_events() {
match event {
RoomEvent::Message {
room: r,
username,
message,
} if r == room => println!("<{username}> {message}"),
RoomEvent::UserJoined { room: r, username } if r == room => {
println!("→ {username} joined");
}
RoomEvent::UserLeft { room: r, username } if r == room => {
println!("← {username} left");
}
_ => {}
}
}
std::thread::sleep(Duration::from_millis(200));
}
let _ = client.leave_room(room);
Ok(())
}
fn send_private_message(
settings: &ClientSettings,
recipient: &str,