diff --git a/Cargo.toml b/Cargo.toml index 5399930..9a0b12c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,3 +8,42 @@ authors = ["Michel de Graaf"] edition = "2024" license = "MIT" repository = "https://github.com/michel/soulseek-rs" + +[workspace.lints.rust] +unused_lifetimes = "warn" +unsafe_op_in_unsafe_fn = "warn" + +# Lints are cranked to the maximum practical level: the full clippy `all`, +# `pedantic`, and `nursery` groups are enabled as warnings. The allow-list below +# opts out of the pedantic/nursery lints that this project intentionally does +# not follow (documentation-style requirements, deliberate numeric casts in the +# protocol/UI layers, and a few noisy nursery lints), so that `cargo clippy` +# stays clean without churning code for no real benefit. +[workspace.lints.clippy] +all = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +nursery = { level = "warn", priority = -1 } +# Documentation-style pedantic lints — not tracked in rustdoc for this project. +missing_errors_doc = "allow" +missing_panics_doc = "allow" +doc_markdown = "allow" +# Numeric casts in the protocol framing and TUI layout are intentional and +# reviewed (the one real width-truncation bug was fixed separately). +cast_possible_truncation = "allow" +cast_precision_loss = "allow" +cast_sign_loss = "allow" +cast_possible_wrap = "allow" +# Message/handler APIs take ownership by design. +needless_pass_by_value = "allow" +# Long event loops and protocol parsers are clearer kept whole. +too_many_lines = "allow" +# Protocol test fixtures are raw byte/number arrays. +unreadable_literal = "allow" +# Nursery lint that fights RAII lock guards with frequent false positives. +significant_drop_tightening = "allow" +# Subjective nursery lint: the suggested map_or/map_or_else rewrites read worse +# than the explicit if let/match they replace. +option_if_let_else = "allow" +# The MD5 round functions use the conventional a/b/c/d register names from the +# RFC 1321 reference implementation. +many_single_char_names = "allow" diff --git a/soulseek-rs-lib/Cargo.toml b/soulseek-rs-lib/Cargo.toml index 9f3be38..a2e8f7b 100644 --- a/soulseek-rs-lib/Cargo.toml +++ b/soulseek-rs-lib/Cargo.toml @@ -13,4 +13,7 @@ categories = ["network-programming"] name = "soulseek_rs" path = "src/lib.rs" +[lints] +workspace = true + [dependencies] diff --git a/soulseek-rs-lib/src/actor/mod.rs b/soulseek-rs-lib/src/actor/mod.rs index a01a6ca..b64f690 100644 --- a/soulseek-rs-lib/src/actor/mod.rs +++ b/soulseek-rs-lib/src/actor/mod.rs @@ -41,14 +41,14 @@ impl ActorHandle { pub fn send(&self, msg: M) -> Result<(), String> { self.sender .send(ActorMessage::UserMessage(msg)) - .map_err(|e| format!("Failed to send message: {}", e)) + .map_err(|e| format!("Failed to send message: {e}")) } /// Request actor to stop gracefully pub fn stop(&self) -> Result<(), String> { self.sender .send(ActorMessage::Stop) - .map_err(|e| format!("Failed to send stop signal: {}", e)) + .map_err(|e| format!("Failed to send stop signal: {e}")) } } @@ -64,16 +64,15 @@ pub struct ActorSystem { } impl ActorSystem { - pub fn new(thread_pool: Arc) -> Self { - ActorSystem { thread_pool } + #[must_use] + pub const fn new(thread_pool: Arc) -> Self { + Self { thread_pool } } /// Spawn a new actor and return its handle pub fn spawn(&self, mut actor: A) -> ActorHandle { let (sender, receiver) = channel::>(); - let handle = ActorHandle { - sender: sender.clone(), - }; + let handle = ActorHandle { sender }; // Start the actor event loop on the thread pool self.thread_pool.execute(move || { @@ -86,7 +85,7 @@ impl ActorSystem { } /// Spawn a new actor with initialization callback and return its handle - /// The callback receives the actor handle before on_start is called + /// The callback receives the actor handle before `on_start` is called pub fn spawn_with_handle( &self, mut actor: A, @@ -96,9 +95,7 @@ impl ActorSystem { F: FnOnce(&mut A, ActorHandle) + Send + 'static, { let (sender, receiver) = channel::>(); - let handle = ActorHandle { - sender: sender.clone(), - }; + let handle = ActorHandle { sender }; let handle_for_init = handle.clone(); self.thread_pool.execute(move || { diff --git a/soulseek-rs-lib/src/actor/peer_actor.rs b/soulseek-rs-lib/src/actor/peer_actor.rs index 8d894cd..66eb84c 100644 --- a/soulseek-rs-lib/src/actor/peer_actor.rs +++ b/soulseek-rs-lib/src/actor/peer_actor.rs @@ -52,6 +52,7 @@ pub struct PeerActor { } impl PeerActor { + #[must_use] pub fn new( peer: Peer, stream: Option, @@ -135,9 +136,9 @@ impl PeerActor { msgs }); - messages - .iter() - .for_each(|msg| self.handle_message(msg.clone())); + for msg in &messages { + self.handle_message(msg.clone()); + } } fn handle_message(&mut self, msg: PeerMessage) { @@ -210,14 +211,7 @@ impl PeerActor { username, token, allowed ); - if !allowed { - if let Some(reason_text) = reason { - debug!( - "[peer:{}] Transfer rejected: {} - token {}, waiting for TransferRequest...", - username, reason_text, token - ); - } - } else { + if allowed { debug!( "[peer:{}] Transfer allowed, ready to connect with token {:}", username, token @@ -237,6 +231,11 @@ impl PeerActor { username, e ); } + } else if let Some(reason_text) = reason { + debug!( + "[peer:{}] Transfer rejected: {} - token {}, waiting for TransferRequest...", + username, reason_text, token + ); } } PeerMessage::PlaceInQueueResponse { filename, place } => { @@ -267,7 +266,7 @@ impl PeerActor { match self.peer.write_safe() { Ok(mut p) => p.username = username, Err(e) => { - error!("[peer_actor] SetUsername write: {}", e) + error!("[peer_actor] SetUsername write: {}", e); } } } @@ -306,9 +305,8 @@ impl PeerActor { } { - let stream = match self.stream.as_mut() { - Some(s) => s, - None => return, + let Some(stream) = self.stream.as_mut() else { + return; }; match self.reader.read_from_socket(stream) { @@ -352,7 +350,7 @@ impl PeerActor { message .get_message_name( MessageType::Peer, - message.get_message_code() as u32 + u32::from(message.get_message_code()) ) .map_err(|e| e.to_string()) ); @@ -381,12 +379,9 @@ impl PeerActor { fn send_message(&mut self, message: Message) { let username = self.peer_username(); - let stream = match self.stream.as_mut() { - Some(s) => s, - None => { - error!("Cannot send message: stream is None"); - return; - } + let Some(stream) = self.stream.as_mut() else { + error!("Cannot send message: stream is None"); + return; }; trace!( @@ -456,7 +451,7 @@ impl PeerActor { }; let socket_addr = - format!("{}:{}", host, port).parse::(); + format!("{host}:{port}").parse::(); match socket_addr { Ok(addr) => { diff --git a/soulseek-rs-lib/src/actor/peer_registry.rs b/soulseek-rs-lib/src/actor/peer_registry.rs index 01b14cc..ad72102 100644 --- a/soulseek-rs-lib/src/actor/peer_registry.rs +++ b/soulseek-rs-lib/src/actor/peer_registry.rs @@ -18,6 +18,7 @@ pub struct PeerRegistry { } impl PeerRegistry { + #[must_use] pub fn new( actor_system: Arc, client_channel: Sender, @@ -48,7 +49,7 @@ impl PeerRegistry { let mut peers = self .peers .lock_safe() - .map_err(|e| format!("peer registry lock poisoned: {}", e))?; + .map_err(|e| format!("peer registry lock poisoned: {e}"))?; // Stop any actor already registered under this username so it does not // become an orphan pinning a pool worker forever (and so its later // shutdown cannot remove this new connection from the registry). @@ -64,6 +65,7 @@ impl PeerRegistry { Ok(handle) } + #[must_use] pub fn get_peer(&self, username: &str) -> Option> { match self.peers.lock_safe() { Ok(peers) => peers.get(username).cloned(), @@ -74,6 +76,7 @@ impl PeerRegistry { } } + #[must_use] pub fn remove_peer( &self, username: &str, @@ -94,6 +97,7 @@ impl PeerRegistry { handle } + #[must_use] pub fn get_all_usernames(&self) -> Vec { match self.peers.lock_safe() { Ok(peers) => peers.keys().cloned().collect(), @@ -104,6 +108,7 @@ impl PeerRegistry { } } + #[must_use] pub fn count(&self) -> usize { match self.peers.lock_safe() { Ok(peers) => peers.len(), @@ -114,6 +119,7 @@ impl PeerRegistry { } } + #[must_use] pub fn contains(&self, username: &str) -> bool { match self.peers.lock_safe() { Ok(peers) => peers.contains_key(username), @@ -129,9 +135,9 @@ impl PeerRegistry { username: &str, message: PeerMessage, ) -> Result<(), String> { - let handle = self.get_peer(username).ok_or_else(|| { - format!("Peer {} not found in registry", username) - })?; + let handle = self + .get_peer(username) + .ok_or_else(|| format!("Peer {username} not found in registry"))?; handle.send(message) } diff --git a/soulseek-rs-lib/src/actor/server_actor.rs b/soulseek-rs-lib/src/actor/server_actor.rs index a2e6119..df82e5f 100644 --- a/soulseek-rs-lib/src/actor/server_actor.rs +++ b/soulseek-rs-lib/src/actor/server_actor.rs @@ -34,15 +34,18 @@ pub struct PeerAddress { } impl PeerAddress { - pub fn new(host: String, port: u16) -> Self { + #[must_use] + pub const fn new(host: String, port: u16) -> Self { Self { host, port } } + #[must_use] pub fn get_host(&self) -> &str { &self.host } - pub fn get_port(&self) -> u16 { + #[must_use] + pub const fn get_port(&self) -> u16 { self.port } } @@ -59,6 +62,7 @@ pub struct Context { } impl Context { + #[must_use] pub fn new() -> Self { Self::default() } @@ -72,7 +76,8 @@ pub struct UserMessage { new_message: bool, } impl UserMessage { - pub fn new( + #[must_use] + pub const fn new( id: u32, timestamp: u32, username: String, @@ -143,6 +148,7 @@ pub struct ServerActor { } impl ServerActor { + #[must_use] pub fn new( address: PeerAddress, client_channel: Sender, @@ -166,11 +172,13 @@ impl ServerActor { } } - pub fn get_address(&self) -> &PeerAddress { + #[must_use] + pub const fn get_address(&self) -> &PeerAddress { &self.address } - pub fn get_sender(&self) -> Option<&Sender> { + #[must_use] + pub const fn get_sender(&self) -> Option<&Sender> { self.dispatcher_sender.as_ref() } @@ -178,7 +186,7 @@ impl ServerActor { let host = self.address.host.clone(); let port = self.address.port; - let addr_str = format!("{}:{}", host, port); + let addr_str = format!("{host}:{port}"); let mut socket_addrs = match addr_str.to_socket_addrs() { Ok(addrs) => addrs, @@ -197,7 +205,7 @@ impl ServerActor { let Some(addr) = socket_addr else { let error_msg = - format!("No socket addresses found for {}:{}", host, port); + format!("No socket addresses found for {host}:{port}"); error!("[server] {}", error_msg); self.disconnect_with_error(io::Error::new( io::ErrorKind::InvalidInput, @@ -280,9 +288,9 @@ impl ServerActor { msgs }); - messages - .iter() - .for_each(|msg| self.handle_message(msg.clone())); + for msg in &messages { + self.handle_message(msg.clone()); + } } pub fn login( @@ -305,7 +313,7 @@ impl ServerActor { } { - logged_in = context.read_safe()?.logged_in + logged_in = context.read_safe()?.logged_in; } if logged_in.is_some() { @@ -341,15 +349,12 @@ impl ServerActor { fn handle_message(&mut self, msg: ServerMessage) { if !matches!(self.connection_state, ConnectionState::Connected) { - match &msg { - ServerMessage::ProcessRead => { - // Always process read operations - } - _ => { - // Queue all other messages when not connected - self.queued_messages.push(msg); - return; - } + if matches!(&msg, ServerMessage::ProcessRead) { + // Always process read operations + } else { + // Queue all other messages when not connected + self.queued_messages.push(msg); + return; } } @@ -357,7 +362,7 @@ impl ServerActor { ServerMessage::ConnectToPeer(peer) => { if let Some(op) = match peer.connection_type { ConnectionType::P | ConnectionType::F => { - Some(ClientOperation::ConnectToPeer(peer.clone())) + Some(ClientOperation::ConnectToPeer(peer)) } ConnectionType::D => None, } && let Err(e) = self.client_channel.send(op) @@ -369,7 +374,7 @@ impl ServerActor { match self.context.write_safe() { Ok(mut ctx) => ctx.logged_in = Some(message), Err(e) => { - error!("[server] LoginStatus write: {}", e) + error!("[server] LoginStatus write: {}", e); } } } @@ -469,9 +474,8 @@ impl ServerActor { } { - let stream = match self.stream.as_mut() { - Some(s) => s, - None => return, + let Some(stream) = self.stream.as_mut() else { + return; }; match self.reader.read_from_socket(stream) { @@ -506,7 +510,7 @@ impl ServerActor { message .get_message_name( MessageType::Server, - message.get_message_code() as u32 + u32::from(message.get_message_code()) ) .map_err(|e| e.to_string()) ); @@ -536,7 +540,7 @@ impl ServerActor { fn queue_message(&mut self, message: Message) { if let Some(sender) = &self.dispatcher_sender { match sender.send(ServerMessage::SendMessage(message)) { - Ok(_) => {} + Ok(()) => {} Err(e) => error!("Failed to send: {}", e), } } else { @@ -546,12 +550,9 @@ impl ServerActor { } fn send_message(&mut self, message: Message) { - let stream = match self.stream.as_mut() { - Some(s) => s, - None => { - error!("[server] Cannot send message: stream is None"); - return; - } + let Some(stream) = self.stream.as_mut() else { + error!("[server] Cannot send message: stream is None"); + return; }; trace!( diff --git a/soulseek-rs-lib/src/client.rs b/soulseek-rs-lib/src/client.rs index a5b1171..fe1aaff 100644 --- a/soulseek-rs-lib/src/client.rs +++ b/soulseek-rs-lib/src/client.rs @@ -114,6 +114,7 @@ impl ClientContext { pub fn remove_download(&mut self, token: u32) { self.downloads.remove(token); } + #[must_use] pub fn get_download_by_token(&self, token: u32) -> Option<&Download> { self.downloads.get_by_token(token) } @@ -130,10 +131,12 @@ impl ClientContext { ) -> Option<&mut Download> { self.downloads.get_by_file_mut(username, filename) } + #[must_use] pub fn get_download_tokens(&self) -> Vec { self.downloads.tokens() } - pub fn get_downloads(&self) -> &Vec { + #[must_use] + pub const fn get_downloads(&self) -> &Vec { self.downloads.list() } pub fn update_download_with_status( @@ -165,7 +168,7 @@ fn test_client_context_downloads() { status: DownloadStatus::Queued, sender: mpsc::channel().0, queue_position: None, - metadata: Default::default(), + metadata: DownloadMetadata::default(), }; context.add_download(download); assert!(context.get_download_by_token(123).is_some()); @@ -173,7 +176,7 @@ fn test_client_context_downloads() { assert_eq!(context.get_downloads().len(), 1); if let Some(download) = context.get_download_by_token_mut(token) { assert_eq!(download.token, token); - download.token = new_token + download.token = new_token; } assert!(context.get_download_by_token(new_token).is_some()); assert_eq!(context.get_download_tokens(), vec![new_token]); @@ -199,7 +202,7 @@ fn test_client_pause_and_resume_download() { }, sender: download_sender, queue_position: None, - metadata: Default::default(), + metadata: DownloadMetadata::default(), }; client.context.write().unwrap().add_download(download); @@ -255,7 +258,7 @@ fn test_client_removes_only_queued_downloads() { status: DownloadStatus::Queued, sender: mpsc::channel().0, queue_position: None, - metadata: Default::default(), + metadata: DownloadMetadata::default(), }; let active_download = Download { username: "peer".to_string(), @@ -270,7 +273,7 @@ fn test_client_removes_only_queued_downloads() { }, sender: mpsc::channel().0, queue_position: None, - metadata: Default::default(), + metadata: DownloadMetadata::default(), }; { @@ -289,12 +292,11 @@ fn test_client_removes_only_queued_downloads() { impl ClientContext { #[must_use] pub fn new() -> Self { - let max_threads = thread::available_parallelism() - .map(|n| n.get()) - .unwrap_or(8); + let max_threads = + thread::available_parallelism().map_or(8, std::num::NonZero::get); let thread_pool = Arc::new(ThreadPool::new(max_threads)); - let actor_system = Arc::new(ActorSystem::new(thread_pool.clone())); + let actor_system = Arc::new(ActorSystem::new(thread_pool)); Self { peer_registry: None, @@ -324,6 +326,7 @@ impl Client { Self::with_settings(ClientSettings::new(username, password)) } + #[must_use] pub fn with_settings(settings: ClientSettings) -> Self { logger::init(); Self { @@ -483,6 +486,7 @@ impl Client { Ok(self.get_search_results(query)) } + #[must_use] pub fn get_search_results_count(&self, search_key: &str) -> usize { self.context .read_safe() @@ -493,6 +497,7 @@ impl Client { .unwrap_or(0) } + #[must_use] pub fn get_search_results(&self, search_key: &str) -> Vec { self.context .read_safe() @@ -504,6 +509,7 @@ impl Client { } /// Non-blocking variant that returns None if the lock is unavailable + #[must_use] pub fn try_get_search_results( &self, search_key: &str, @@ -513,6 +519,7 @@ impl Client { }) } + #[must_use] pub fn get_all_searches(&self) -> HashMap { self.context .read_safe() @@ -520,6 +527,7 @@ impl Client { .unwrap_or_default() } + #[must_use] pub fn get_all_downloads(&self) -> Vec { self.context .read_safe() @@ -527,6 +535,7 @@ impl Client { .unwrap_or_default() } + #[must_use] pub fn pause_download(&self, username: &str, filename: &str) -> bool { match self.context.write_safe() { Ok(mut ctx) => ctx.downloads.pause_by_file(username, filename), @@ -537,6 +546,7 @@ impl Client { } } + #[must_use] pub fn resume_download(&self, username: &str, filename: &str) -> bool { match self.context.write_safe() { Ok(mut ctx) => ctx.downloads.resume_by_file(username, filename), @@ -547,6 +557,7 @@ impl Client { } } + #[must_use] pub fn remove_queued_download( &self, username: &str, @@ -599,7 +610,7 @@ impl Client { let download = Download { username: username.clone(), - filename: filename.clone(), + filename, token, size, download_directory, @@ -728,17 +739,18 @@ impl Client { // client ops loop if this read guard were still // held on this thread. { - let context = - match client_context.read_safe() { - Ok(c) => c, - Err(e) => { - error!( - "[client] PeerDisconnected read: {}", - e - ); - continue; - } - }; + let context = match client_context + .read_safe() + { + Ok(c) => c, + Err(e) => { + error!( + "[client] PeerDisconnected read: {}", + e + ); + continue; + } + }; if let Some(ref registry) = context.peer_registry && let Some(handle) = @@ -864,17 +876,17 @@ impl Client { token ); } - }; + } } ClientOperation::NewPeer(new_peer) => { let peer_exists = match client_context .read_safe() { - Ok(ctx) => ctx - .peer_registry - .as_ref() - .map(|r| r.contains(&new_peer.username)) - .unwrap_or(false), + Ok(ctx) => { + ctx.peer_registry.as_ref().is_some_and( + |r| r.contains(&new_peer.username), + ) + } Err(e) => { error!("[client] NewPeer read: {}", e); continue; @@ -965,8 +977,7 @@ impl Client { Ok(ctx) => ctx .peer_registry .as_ref() - .map(|r| r.contains(&username)) - .unwrap_or(false), + .is_some_and(|r| r.contains(&username)), Err(e) => { error!( "[client] GetPeerAddressResponse read: {}", @@ -1121,7 +1132,7 @@ impl Client { own_username: String, stream: Option, ) { - let client_context = client_context.clone(); + let client_context = client_context; let peer_clone = peer.clone(); trace!( @@ -1130,7 +1141,7 @@ impl Client { ); match peer.connection_type { ConnectionType::P => { - let username = peer.username.clone(); + let username = peer.username; let context = match client_context.read_safe() { Ok(c) => c, @@ -1165,7 +1176,7 @@ impl Client { peer.port, peer.token.unwrap(), false, - own_username.clone(), + own_username, ); match download_peer.download_file( @@ -1196,7 +1207,7 @@ impl Client { } } ConnectionType::D => { - error!("ConnectionType::D not implemented") + error!("ConnectionType::D not implemented"); } } } @@ -1217,9 +1228,9 @@ impl Client { if let Some(server_sender) = &context.server_sender { if let Some(token) = peer.token { match server_sender.send(ServerMessage::PierceFirewall(token)) { - Ok(_) => (), + Ok(()) => (), Err(e) => { - error!("Failed to send PierceFirewall message: {}", e) + error!("Failed to send PierceFirewall message: {}", e); } } } else { diff --git a/soulseek-rs-lib/src/dispatcher.rs b/soulseek-rs-lib/src/dispatcher.rs index d1b1c4f..49f23fd 100644 --- a/soulseek-rs-lib/src/dispatcher.rs +++ b/soulseek-rs-lib/src/dispatcher.rs @@ -10,12 +10,13 @@ pub struct MessageDispatcher { } impl MessageDispatcher { - pub fn new( + #[must_use] + pub const fn new( owner_name: String, sender: Sender, handlers: Handlers, ) -> Self { - MessageDispatcher { + Self { owner_name, sender, handlers, diff --git a/soulseek-rs-lib/src/download_store.rs b/soulseek-rs-lib/src/download_store.rs index ec8f8c1..539acfa 100644 --- a/soulseek-rs-lib/src/download_store.rs +++ b/soulseek-rs-lib/src/download_store.rs @@ -6,6 +6,7 @@ pub struct DownloadStore { } impl DownloadStore { + #[must_use] pub fn new() -> Self { Self::default() } @@ -18,6 +19,7 @@ impl DownloadStore { self.downloads.retain(|d| d.token != token); } + #[must_use] pub fn get_by_token(&self, token: u32) -> Option<&Download> { self.downloads.iter().find(|d| d.token == token) } @@ -36,11 +38,13 @@ impl DownloadStore { .find(|d| d.username == username && d.filename == filename) } + #[must_use] pub fn tokens(&self) -> Vec { self.downloads.iter().map(|d| d.token).collect() } - pub fn list(&self) -> &Vec { + #[must_use] + pub const fn list(&self) -> &Vec { &self.downloads } @@ -128,9 +132,11 @@ impl DownloadStore { } /// Returns the tokens of downloads matching `username` (and optionally a -/// `filename`) after notifying their senders of `Failed`. Caller is -/// responsible for then calling `update_status` and `remove` for each token, -/// typically under a write lock. +/// `filename`) after notifying their senders of `Failed`. +/// +/// Caller is responsible for then calling `update_status` and `remove` for +/// each token, typically under a write lock. +#[must_use] pub fn collect_failed_tokens( store: &DownloadStore, username: &str, @@ -152,19 +158,20 @@ pub fn collect_failed_tokens( #[cfg(test)] mod tests { use super::*; + use crate::types::DownloadMetadata; use std::sync::mpsc; fn make_download(token: u32, status: DownloadStatus) -> Download { Download { username: "peer".to_string(), - filename: format!("file-{}.mp3", token), + filename: format!("file-{token}.mp3"), token, size: 100, download_directory: "test".to_string(), status, sender: mpsc::channel().0, queue_position: None, - metadata: Default::default(), + metadata: DownloadMetadata::default(), } } diff --git a/soulseek-rs-lib/src/error.rs b/soulseek-rs-lib/src/error.rs index 1410dbb..b25df46 100644 --- a/soulseek-rs-lib/src/error.rs +++ b/soulseek-rs-lib/src/error.rs @@ -25,23 +25,23 @@ pub enum SoulseekRs { impl fmt::Display for SoulseekRs { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - SoulseekRs::NetworkError(err) => { - write!(f, "Network error: {}", err) + Self::NetworkError(err) => { + write!(f, "Network error: {err}") } - SoulseekRs::AuthenticationFailed => { + Self::AuthenticationFailed => { write!(f, "Authentication failed") } - SoulseekRs::ParseError(msg) => write!(f, "Parse error: {}", msg), - SoulseekRs::Timeout => write!(f, "Operation timed out"), - SoulseekRs::ConnectionClosed => write!(f, "Connection closed"), - SoulseekRs::InvalidMessage(msg) => { - write!(f, "Invalid message: {}", msg) + Self::ParseError(msg) => write!(f, "Parse error: {msg}"), + Self::Timeout => write!(f, "Operation timed out"), + Self::ConnectionClosed => write!(f, "Connection closed"), + Self::InvalidMessage(msg) => { + write!(f, "Invalid message: {msg}") } - SoulseekRs::NotConnected => write!(f, "Not connected to server"), - SoulseekRs::CompressionError(msg) => { - write!(f, "Compression error: {}", msg) + Self::NotConnected => write!(f, "Not connected to server"), + Self::CompressionError(msg) => { + write!(f, "Compression error: {msg}") } - SoulseekRs::LockPoisoned => { + Self::LockPoisoned => { write!(f, "Lock poisoned by panicking thread") } } @@ -51,7 +51,7 @@ impl fmt::Display for SoulseekRs { impl Error for SoulseekRs { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { - SoulseekRs::NetworkError(err) => Some(err), + Self::NetworkError(err) => Some(err), _ => None, } } @@ -59,19 +59,19 @@ impl Error for SoulseekRs { impl From for SoulseekRs { fn from(err: std::io::Error) -> Self { - SoulseekRs::NetworkError(err) + Self::NetworkError(err) } } impl From for SoulseekRs { fn from(err: std::num::ParseIntError) -> Self { - SoulseekRs::ParseError(format!("Integer parse error: {}", err)) + Self::ParseError(format!("Integer parse error: {err}")) } } impl From for SoulseekRs { fn from(err: String) -> Self { - SoulseekRs::CompressionError(err) + Self::CompressionError(err) } } diff --git a/soulseek-rs-lib/src/message/handlers.rs b/soulseek-rs-lib/src/message/handlers.rs index 0f343c8..03fc764 100644 --- a/soulseek-rs-lib/src/message/handlers.rs +++ b/soulseek-rs-lib/src/message/handlers.rs @@ -18,6 +18,7 @@ impl Default for Handlers { } impl Handlers { + #[must_use] pub fn new() -> Self { Self { handlers: HashMap::new(), @@ -31,6 +32,7 @@ impl Handlers { self.handlers.insert(handler.get_code(), Box::new(handler)); self } + #[must_use] pub fn get_handler( &self, code: u8, diff --git a/soulseek-rs-lib/src/message/message_reader.rs b/soulseek-rs-lib/src/message/message_reader.rs index 25a54fe..b4352fd 100644 --- a/soulseek-rs-lib/src/message/message_reader.rs +++ b/soulseek-rs-lib/src/message/message_reader.rs @@ -18,13 +18,15 @@ impl Default for MessageReader { } impl MessageReader { - pub fn new() -> Self { + #[must_use] + pub const fn new() -> Self { Self { buffer: VecDeque::new(), } } #[cfg(test)] + #[must_use] pub fn new_with_buffer(buffer: Vec) -> Self { Self { buffer: buffer.into(), @@ -47,6 +49,7 @@ impl MessageReader { Ok(()) } + #[must_use] pub fn buffer_len(&self) -> usize { self.buffer.len() } diff --git a/soulseek-rs-lib/src/message/mod.rs b/soulseek-rs-lib/src/message/mod.rs index b097cd6..bf2bcca 100644 --- a/soulseek-rs-lib/src/message/mod.rs +++ b/soulseek-rs-lib/src/message/mod.rs @@ -10,7 +10,7 @@ pub use message_reader::MessageReader; use std::str; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] #[allow(dead_code)] pub enum MessageType { Server, @@ -30,7 +30,7 @@ impl std::fmt::Display for Error { impl std::error::Error for Error {} -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct Message { data: Vec, pointer: usize, @@ -43,7 +43,8 @@ impl Default for Message { } impl Message { - pub fn new() -> Self { + #[must_use] + pub const fn new() -> Self { Self { data: Vec::new(), pointer: 0, @@ -51,8 +52,8 @@ impl Message { } pub fn print_hex(&self) { - let data = &self.data; const BYTES_PER_LINE: usize = 16; + let data = &self.data; let chunks = data.chunks(BYTES_PER_LINE); for (i, chunk) in chunks.enumerate() { @@ -95,6 +96,7 @@ impl Message { } #[allow(dead_code)] + #[must_use] pub fn get_message_code_u32(&self) -> u32 { if self.data.len() < 8 { return 0; @@ -102,6 +104,7 @@ impl Message { u32::from_le_bytes(self.data[4..8].try_into().unwrap()) } + #[must_use] pub fn get_message_code(&self) -> u8 { if self.data.len() <= 4 { return 0; @@ -109,6 +112,7 @@ impl Message { self.data[4] } + #[must_use] pub fn get_message_code_send(&self) -> u8 { if self.data.is_empty() { return 0; @@ -116,34 +120,38 @@ impl Message { self.data[0] } - pub fn new_with_data(data: Vec) -> Self { + #[must_use] + pub const fn new_with_data(data: Vec) -> Self { Self { data, pointer: 0 } } #[allow(dead_code)] - pub fn reset_pointer(&mut self) { + pub const fn reset_pointer(&mut self) { self.pointer = 0; } - pub fn set_pointer(&mut self, pointer: usize) { + pub const fn set_pointer(&mut self, pointer: usize) { self.pointer = pointer; } - pub fn get_pointer(&mut self) -> usize { + pub const fn get_pointer(&mut self) -> usize { self.pointer } - pub fn get_size(&mut self) -> usize { + pub const fn get_size(&mut self) -> usize { self.data.len() } + #[must_use] pub fn get_data(&self) -> Vec { self.data.clone() } + #[must_use] pub fn get_slice(&self, from: usize, to: usize) -> Vec { self.data[from..to].to_vec() } /// gets buffer with the message length prepended + #[must_use] pub fn get_buffer(&self) -> Vec { let mut b = vec![0u8; 4]; let length = self.data.len() as u32; @@ -277,7 +285,7 @@ impl Message { } pub fn write_bool(&mut self, value: bool) -> &mut Self { - self.data.push(if value { 1 } else { 0 }); + self.data.push(u8::from(value)); self } @@ -333,16 +341,13 @@ impl Message { 104 => Ok("WishlistInterval"), 160 => Ok("ExcludedSearchPhrases"), 1001 => Ok("CantConnectToPeer"), - _ => { - Err(Error(format!("Unknown server message code: {}", code))) - } + _ => Err(Error(format!("Unknown server message code: {code}"))), }, MessageType::PeerInit => match code { 0 => Ok("PierceFireWall"), 1 => Ok("PeerInit"), _ => Err(Error(format!( - "Unknown peer init message code: {}", - code + "Unknown peer init message code: {code}" ))), }, MessageType::Peer => match code { @@ -361,7 +366,7 @@ impl Message { 46 => Ok("UploadFailed"), 50 => Ok("UploadDenied"), 51 => Ok("PlaceInQueueRequest"), - _ => Err(Error(format!("Unknown peer message code: {}", code))), + _ => Err(Error(format!("Unknown peer message code: {code}"))), }, MessageType::Distributed => match code { 3 => Ok("SearchRequest"), @@ -369,8 +374,7 @@ impl Message { 5 => Ok("BranchRoot"), 93 => Ok("EmbeddedMessage"), _ => Err(Error(format!( - "Unknown distributed message code: {}", - code + "Unknown distributed message code: {code}" ))), }, } diff --git a/soulseek-rs-lib/src/message/peer/file_search_response.rs b/soulseek-rs-lib/src/message/peer/file_search_response.rs index cd0ea8a..949dc40 100644 --- a/soulseek-rs-lib/src/message/peer/file_search_response.rs +++ b/soulseek-rs-lib/src/message/peer/file_search_response.rs @@ -9,9 +9,9 @@ impl MessageHandler for FileSearchResponse { 9 } fn handle(&self, message: &mut Message, sender: Sender) { - let file_search = match SearchResult::new_from_message(message) { - Ok(result) => result, - Err(_) => return, // Skip malformed search results + // Skip malformed search results + let Ok(file_search) = SearchResult::new_from_message(message) else { + return; }; sender diff --git a/soulseek-rs-lib/src/message/server/get_peer_address.rs b/soulseek-rs-lib/src/message/server/get_peer_address.rs index d20a41a..d5b28ee 100644 --- a/soulseek-rs-lib/src/message/server/get_peer_address.rs +++ b/soulseek-rs-lib/src/message/server/get_peer_address.rs @@ -22,7 +22,7 @@ impl MessageHandler for GetPeerAddressHandler { let port = message.read_int32(); let obfuscation_type = message.read_int32(); let obfuscated_port = message.read_int32() as u16; - println!("GetPeerAddressHandler: {:?}", username); // Debug print + println!("GetPeerAddressHandler: {username:?}"); // Debug print sender .send(ServerMessage::GetPeerAddressResponse { diff --git a/soulseek-rs-lib/src/message/server/message_factory.rs b/soulseek-rs-lib/src/message/server/message_factory.rs index 264b79c..6df5742 100644 --- a/soulseek-rs-lib/src/message/server/message_factory.rs +++ b/soulseek-rs-lib/src/message/server/message_factory.rs @@ -4,6 +4,7 @@ use crate::{ pub struct MessageFactory; impl MessageFactory { + #[must_use] pub fn build_get_peer_address(username: &str) -> Message { let mut message = Message::new(); @@ -11,6 +12,7 @@ impl MessageFactory { message.write_string(username); message } + #[must_use] pub fn build_login_message(username: &str, password: &str) -> Message { // Message::new_with_data( // [ @@ -42,6 +44,7 @@ impl MessageFactory { .clone() } + #[must_use] pub fn build_shared_folders_message( folder_count: u32, file_count: u32, @@ -52,6 +55,7 @@ impl MessageFactory { .write_int32(file_count) .clone() } + #[must_use] pub fn build_file_search_message(token: u32, query: &str) -> Message { Message::new() .write_int32(26) @@ -59,21 +63,25 @@ impl MessageFactory { .write_string(query) .clone() } + #[must_use] pub fn build_set_status_message(status_code: u32) -> Message { Message::new() .write_int32(28) .write_int32(status_code) .clone() } + #[must_use] pub fn build_no_parent_message() -> Message { Message::new().write_int32(71).write_bool(true).clone() } + #[must_use] pub fn build_set_wait_port_message(port: u16) -> Message { Message::new() .write_int32(2) .write_int32(port.into()) .clone() } + #[must_use] pub fn build_watch_user(token: u32) -> Message { Message::new() .write_raw_bytes([5, 0, 0, 0, 0].to_vec()) @@ -81,6 +89,7 @@ impl MessageFactory { .clone() } + #[must_use] pub fn build_queue_upload_message(filename: &str) -> Message { Message::new() .write_int32(43) @@ -88,6 +97,7 @@ impl MessageFactory { .clone() } + #[must_use] pub fn build_transfer_request_message( filename: &str, token: u32, @@ -99,6 +109,7 @@ impl MessageFactory { .write_string(filename) .clone() } + #[must_use] pub fn build_transfer_response_message(transfer: Transfer) -> Message { Message::new() .write_int32(41) @@ -106,6 +117,7 @@ impl MessageFactory { .write_bool(true) .clone() } + #[must_use] pub fn build_pierce_firewall_message(token: u32) -> Message { Message::new() .write_int8(0) // PierceFirewall message code @@ -114,6 +126,7 @@ impl MessageFactory { } #[allow(dead_code)] + #[must_use] pub fn build_peer_init_message( own_username: &str, connection_type: ConnectionType, @@ -134,7 +147,7 @@ fn test_build_watch_user() { let message = MessageFactory::build_watch_user(token); let expect: Vec = [5, 0, 0, 0, 0, 223, 0, 0, 0].to_vec(); - assert_eq!(expect, message.get_data()) + assert_eq!(expect, message.get_data()); } #[test] @@ -151,7 +164,7 @@ fn test_build_login_message() { ] .to_vec(); - assert_eq!(expect, message.get_data()) + assert_eq!(expect, message.get_data()); } #[test] @@ -162,5 +175,5 @@ fn test_build_file_search_message() { 119, 97, 120, ] .to_vec(); - assert_eq!(expect, message.get_data()) + assert_eq!(expect, message.get_data()); } diff --git a/soulseek-rs-lib/src/message/server/message_user.rs b/soulseek-rs-lib/src/message/server/message_user.rs index 9fbe35c..47ae544 100644 --- a/soulseek-rs-lib/src/message/server/message_user.rs +++ b/soulseek-rs-lib/src/message/server/message_user.rs @@ -20,12 +20,12 @@ impl MessageHandler for MessageUser { let user_message = UserMessage::new( id, timestamp, - username.clone(), + username, message_content, new_message, ); info!("[MessageUser] User message received:{:?}", user_message); - user_message.print() + user_message.print(); } } diff --git a/soulseek-rs-lib/src/peer/download_peer.rs b/soulseek-rs-lib/src/peer/download_peer.rs index bc6b161..380757c 100644 --- a/soulseek-rs-lib/src/peer/download_peer.rs +++ b/soulseek-rs-lib/src/peer/download_peer.rs @@ -37,22 +37,22 @@ pub enum DownloadError { impl std::fmt::Display for DownloadError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::ConnectionFailed(e) => write!(f, "Connection failed: {}", e), + Self::ConnectionFailed(e) => write!(f, "Connection failed: {e}"), Self::InvalidAddress(addr) => { - write!(f, "Invalid address: {}", addr) + write!(f, "Invalid address: {addr}") } - Self::HandshakeFailed(e) => write!(f, "Handshake failed: {}", e), - Self::StreamReadError(e) => write!(f, "Stream read error: {}", e), - Self::StreamWriteError(e) => write!(f, "Stream write error: {}", e), + Self::HandshakeFailed(e) => write!(f, "Handshake failed: {e}"), + Self::StreamReadError(e) => write!(f, "Stream read error: {e}"), + Self::StreamWriteError(e) => write!(f, "Stream write error: {e}"), Self::TokenNotFound(token) => { - write!(f, "Token not found: {}", token) + write!(f, "Token not found: {token}") } Self::DownloadInfoMissing(token) => { - write!(f, "Download info missing for token: {}", token) + write!(f, "Download info missing for token: {token}") } - Self::FileWriteError(e) => write!(f, "File write error: {}", e), + Self::FileWriteError(e) => write!(f, "File write error: {e}"), Self::PathResolutionError(msg) => { - write!(f, "Path resolution error: {}", msg) + write!(f, "Path resolution error: {msg}") } Self::InvalidTokenBytes => { write!(f, "Invalid token bytes received") @@ -100,7 +100,7 @@ struct StreamProcessor { } impl StreamProcessor { - fn new() -> Self { + const fn new() -> Self { Self { total_bytes: 0, received: false, @@ -113,7 +113,7 @@ impl StreamProcessor { self.total_bytes += data.len(); } - fn should_continue(&self, expected_size: Option) -> bool { + const fn should_continue(&self, expected_size: Option) -> bool { if let Some(size) = expected_size { self.total_bytes < size } else { @@ -134,7 +134,7 @@ pub struct DownloadPeer { impl DownloadPeer { #[must_use] - pub fn new( + pub const fn new( username: String, host: String, port: u32, @@ -277,7 +277,7 @@ impl DownloadPeer { loop { if let Some(ref dl) = download { - self.wait_while_paused(client_context, dl)?; + Self::wait_while_paused(client_context, dl)?; } match stream.read(&mut read_buffer) { @@ -387,7 +387,6 @@ impl DownloadPeer { } fn wait_while_paused( - &self, client_context: &Arc>, download: &Download, ) -> Result<(), DownloadError> { @@ -408,7 +407,6 @@ impl DownloadPeer { } fn resolve_download_path( - &self, download: &Download, ) -> Result { let download_directory = &download.download_directory; @@ -462,7 +460,6 @@ impl DownloadPeer { } fn save_downloaded_file( - &self, path: &str, data: &[u8], ) -> Result<(), DownloadError> { @@ -511,8 +508,8 @@ impl DownloadPeer { let (buffer, download) = self.read_download_stream(&mut stream, &client_context, download)?; - let final_path = self.resolve_download_path(&download)?; - self.save_downloaded_file(&final_path, &buffer)?; + let final_path = Self::resolve_download_path(&download)?; + Self::save_downloaded_file(&final_path, &buffer)?; trace!( "[download_peer:{}] download completed successfully: {} bytes, saved to: {}", @@ -533,7 +530,8 @@ mod tests { fn finalize_rejects_truncated_download() { // Peer closed early: 5 of 10 promised bytes. Must be a failure so the // partial file is never reported as Completed. - let result = DownloadPeer::finalize_download_buffer(vec![1, 2, 3, 4, 5], 10); + let result = + DownloadPeer::finalize_download_buffer(vec![1, 2, 3, 4, 5], 10); assert!(matches!( result, Err(DownloadError::IncompleteDownload { diff --git a/soulseek-rs-lib/src/peer/listen.rs b/soulseek-rs-lib/src/peer/listen.rs index af63fd2..bfdefec 100644 --- a/soulseek-rs-lib/src/peer/listen.rs +++ b/soulseek-rs-lib/src/peer/listen.rs @@ -65,7 +65,7 @@ fn parse_token_from_buffer(buffer: &[u8], username: &str) -> Option { let token = u32::from_le_bytes( token_bytes .try_into() - .unwrap_or_else(|_| panic!("[listener:{}] slice with incorrect length, can't extract transfer_token", username)), + .unwrap_or_else(|_| panic!("[listener:{username}] slice with incorrect length, can't extract transfer_token")), ); Some(token) } @@ -182,7 +182,7 @@ fn handle_file_connection( DownloadStatus::Completed, ), Err(e) => { - error!("[listener] handle_file_connection write: {}", e) + error!("[listener] handle_file_connection write: {}", e); } } info!( @@ -204,7 +204,10 @@ fn handle_file_connection( DownloadStatus::Failed, ), Err(e) => { - error!("[listener] handle_file_connection fail write: {}", e) + error!( + "[listener] handle_file_connection fail write: {}", + e + ); } } } @@ -272,7 +275,7 @@ fn handle_incoming_connection(stream: TcpStream, context: ConnectionContext) { &context, &peer_ip, peer_port, - ) + ); }); } ConnectionType::D => { diff --git a/soulseek-rs-lib/src/peer/mod.rs b/soulseek-rs-lib/src/peer/mod.rs index 541d0f8..f6cf6fc 100644 --- a/soulseek-rs-lib/src/peer/mod.rs +++ b/soulseek-rs-lib/src/peer/mod.rs @@ -60,9 +60,9 @@ impl FromStr for ConnectionType { fn from_str(s: &str) -> Result { match s { - "P" => Ok(ConnectionType::P), - "F" => Ok(ConnectionType::F), - "D" => Ok(ConnectionType::D), + "P" => Ok(Self::P), + "F" => Ok(Self::F), + "D" => Ok(Self::D), _ => Err(ParseConnectionTypeError), } } @@ -71,11 +71,11 @@ impl FromStr for ConnectionType { impl fmt::Display for ConnectionType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let s = match self { - ConnectionType::P => "P", - ConnectionType::F => "F", - ConnectionType::D => "D", + Self::P => "P", + Self::F => "F", + Self::D => "D", }; - write!(f, "{}", s) + write!(f, "{s}") } } @@ -93,7 +93,8 @@ pub struct Peer { } impl Peer { #[allow(clippy::too_many_arguments, dead_code)] - pub fn new( + #[must_use] + pub const fn new( username: String, connection_type: ConnectionType, host: String, diff --git a/soulseek-rs-lib/src/types.rs b/soulseek-rs-lib/src/types.rs index b2e992c..bd520b8 100644 --- a/soulseek-rs-lib/src/types.rs +++ b/soulseek-rs-lib/src/types.rs @@ -109,7 +109,8 @@ pub struct Download { } impl Download { - pub fn is_finished(&self) -> bool { + #[must_use] + pub const fn is_finished(&self) -> bool { matches!( self.status, DownloadStatus::Completed @@ -118,7 +119,8 @@ impl Download { ) } - pub fn bytes_downloaded(&self) -> u64 { + #[must_use] + pub const fn bytes_downloaded(&self) -> u64 { match &self.status { DownloadStatus::InProgress { bytes_downloaded, .. @@ -131,7 +133,8 @@ impl Download { } } - pub fn speed_bytes_per_sec(&self) -> f64 { + #[must_use] + pub const fn speed_bytes_per_sec(&self) -> f64 { match &self.status { DownloadStatus::InProgress { speed_bytes_per_sec, diff --git a/soulseek-rs-lib/src/utils/logger.rs b/soulseek-rs-lib/src/utils/logger.rs index 72153c4..3a00f36 100644 --- a/soulseek-rs-lib/src/utils/logger.rs +++ b/soulseek-rs-lib/src/utils/logger.rs @@ -32,12 +32,10 @@ pub fn init() { unsafe { LOG_LEVEL = match level.as_str() { "ERROR" => LogLevel::Error, - "WARN" => LogLevel::Warn, "INFO" => LogLevel::Info, - "DEBUG" => LogLevel::Debug, + "DEBUG" | "VERBOSE" => LogLevel::Debug, // Map VERBOSE to DEBUG "TRACE" => LogLevel::Trace, - "VERBOSE" => LogLevel::Debug, // Map VERBOSE to DEBUG - _ => LogLevel::Warn, // Default to WARN + _ => LogLevel::Warn, // "WARN" or default }; } @@ -54,10 +52,7 @@ pub fn init() { } } Err(e) => { - eprintln!( - "Failed to open log file '{}': {}", - log_file_path, e - ); + eprintln!("Failed to open log file '{log_file_path}': {e}"); } } } @@ -74,7 +69,7 @@ enum LogSink { Stderr, } -fn choose_sink(buffering: bool, has_file: bool) -> LogSink { +const fn choose_sink(buffering: bool, has_file: bool) -> LogSink { match (buffering, has_file) { // A configured file always takes the line directly and bypasses // buffering, so the flush step never re-writes it. @@ -86,7 +81,7 @@ fn choose_sink(buffering: bool, has_file: bool) -> LogSink { } fn has_log_file() -> bool { - LOG_FILE.lock().map(|f| f.is_some()).unwrap_or(false) + LOG_FILE.lock().is_ok_and(|f| f.is_some()) } pub fn log(level: LogLevel, message: &str) { @@ -161,29 +156,11 @@ pub fn log(level: LogLevel, message: &str) { }; let formatted_message = format!( - "[{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:03}] [{}] {}", - year, - month, - day, - hours, - minutes, - seconds, - subsec_millis, - level_str, - message + "[{year:04}-{month:02}-{day:02} {hours:02}:{minutes:02}:{seconds:02}.{subsec_millis:03}] [{level_str}] {message}" ); let formatted_message_plain = format!( - "[{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:03}] [{}] {}", - year, - month, - day, - hours, - minutes, - seconds, - subsec_millis, - level_str_plain, - message + "[{year:04}-{month:02}-{day:02} {hours:02}:{minutes:02}:{seconds:02}.{subsec_millis:03}] [{level_str_plain}] {message}" ); match choose_sink(BUFFERING.load(Ordering::Relaxed), has_log_file()) @@ -192,18 +169,17 @@ pub fn log(level: LogLevel, message: &str) { if let Ok(mut log_file) = LOG_FILE.lock() && let Some(file) = log_file.as_mut() { - let _ = - writeln!(file, "{}", formatted_message_plain); + let _ = writeln!(file, "{formatted_message_plain}"); let _ = file.flush(); } } LogSink::Buffer => { if let Ok(mut buffer) = BUFFER.lock() { - buffer.push(formatted_message.clone()); + buffer.push(formatted_message); } } LogSink::Stderr => { - eprintln!("{}", formatted_message); + eprintln!("{formatted_message}"); } } } @@ -228,18 +204,18 @@ pub fn flush_buffered_logs() { for message in buffer.iter() { // Strip ANSI codes for file output let plain_message = strip_ansi_codes(message); - let _ = writeln!(file, "{}", plain_message); + let _ = writeln!(file, "{plain_message}"); } let _ = file.flush(); } else { for message in buffer.iter() { - eprintln!("{}", message); + eprintln!("{message}"); } } } else { // If lock fails, fall back to stderr for message in buffer.iter() { - eprintln!("{}", message); + eprintln!("{message}"); } } buffer.clear(); @@ -270,26 +246,6 @@ fn strip_ansi_codes(s: &str) -> String { result } -#[cfg(test)] -mod tests { - use super::{LogSink, choose_sink}; - - #[test] - fn a_configured_file_bypasses_buffering_so_lines_are_not_duplicated() { - // With a file configured, the line goes straight to the file whether or - // not buffering is on, so it is never also queued for a flush that would - // write it a second time. - assert_eq!(choose_sink(true, true), LogSink::File); - assert_eq!(choose_sink(false, true), LogSink::File); - } - - #[test] - fn without_a_file_buffering_routes_to_the_buffer_else_stderr() { - assert_eq!(choose_sink(true, false), LogSink::Buffer); - assert_eq!(choose_sink(false, false), LogSink::Stderr); - } -} - #[macro_export] macro_rules! error { ($($arg:tt)*) => { @@ -324,3 +280,23 @@ macro_rules! trace { $crate::utils::logger::log($crate::utils::logger::LogLevel::Trace, &format!($($arg)*)) }; } + +#[cfg(test)] +mod tests { + use super::{LogSink, choose_sink}; + + #[test] + fn a_configured_file_bypasses_buffering_so_lines_are_not_duplicated() { + // With a file configured, the line goes straight to the file whether or + // not buffering is on, so it is never also queued for a flush that would + // write it a second time. + assert_eq!(choose_sink(true, true), LogSink::File); + assert_eq!(choose_sink(false, true), LogSink::File); + } + + #[test] + fn without_a_file_buffering_routes_to_the_buffer_else_stderr() { + assert_eq!(choose_sink(true, false), LogSink::Buffer); + assert_eq!(choose_sink(false, false), LogSink::Stderr); + } +} diff --git a/soulseek-rs-lib/src/utils/md5.rs b/soulseek-rs-lib/src/utils/md5.rs index dc0c3e9..c06becd 100644 --- a/soulseek-rs-lib/src/utils/md5.rs +++ b/soulseek-rs-lib/src/utils/md5.rs @@ -22,7 +22,7 @@ use std::convert::TryInto; * K[i] = floor(2^32 * abs(sin(i+1))) */ fn table_construction_function(i: u32) -> u32 { - let x: f64 = i as f64; + let x: f64 = f64::from(i); let sin_eval = x.sin().abs(); // note: 4294967296 == 2^32 (4294967296.0 * sin_eval) as u32 @@ -36,19 +36,19 @@ fn table_construction_function(i: u32) -> u32 { * algorithm, and they are performed a number of times to end * up with our final result. */ -fn f(x: u32, y: u32, z: u32) -> u32 { +const fn f(x: u32, y: u32, z: u32) -> u32 { x & y | !x & z } -fn g(x: u32, y: u32, z: u32) -> u32 { +const fn g(x: u32, y: u32, z: u32) -> u32 { x & z | y & !z } -fn h(x: u32, y: u32, z: u32) -> u32 { +const fn h(x: u32, y: u32, z: u32) -> u32 { x ^ y ^ z } -fn i(x: u32, y: u32, z: u32) -> u32 { +const fn i(x: u32, y: u32, z: u32) -> u32 { y ^ (x | !z) } @@ -67,7 +67,7 @@ fn vec_to_array(v: Vec) -> [T; N] { * table[i] is an entry generated by our sine function, s is an unsigned * integer, and a,b,c,d are our 32 bit registers we perform operations on. */ -fn round_one_operations( +const fn round_one_operations( mut a: u32, mut b: u32, mut c: u32, @@ -115,7 +115,7 @@ fn round_one_operations( * Round 2 is equivalent to round 1, except we swap out our f function for g as such: * a = b + ((a + g(b,c,d) + x[k] + table[i]) <<< s); */ -fn round_two_operations( +const fn round_two_operations( mut a: u32, mut b: u32, mut c: u32, @@ -161,7 +161,7 @@ fn round_two_operations( * Round 3 is equivalent to round 1, except we swap out our f function for h as such: * a = b + ((a + h(b,c,d) + x[k] + table[i]) <<< s); */ -fn round_three_operations( +const fn round_three_operations( mut a: u32, mut b: u32, mut c: u32, @@ -207,7 +207,7 @@ fn round_three_operations( * Round 4 is equivalent to round 1, except we swap out our f function for h as such: * a = b + ((a + i(b,c,d) + x[k] + table[i]) <<< s); */ -fn round_four_operations( +const fn round_four_operations( mut a: u32, mut b: u32, mut c: u32, @@ -287,7 +287,7 @@ fn compute_md5_digest(mut v: Vec) -> String { let table = construct_value_table(); // let M[0 .. N-1] = words of resulting message, where N is multiple of 16 - for chunk in v.chunks_exact_mut(64) { + for chunk in v.as_chunks_mut::<64>().0 { let x = convert_u8_chunk_to_u32(chunk); // set all values of a,b,c,d to aa,bb,cc, and dd respectively. @@ -378,7 +378,7 @@ fn bit_padding(input: &str) -> Vec { input_vector } -fn split_u64_to_u8_array(s: u64) -> [u8; 8] { +const fn split_u64_to_u8_array(s: u64) -> [u8; 8] { [ s as u8, (s >> 8) as u8, @@ -425,6 +425,7 @@ fn convert_str_to_vec(input: &str) -> Vec { * during our bitwise operations to retrieve our message digest. * (This is a simplification, but describes the core functionality) */ +#[must_use] pub fn md5(input: &str) -> String { let input_vec = bit_padding(input); compute_md5_digest(input_vec) @@ -454,7 +455,7 @@ fn lowercase_alphabet_correct_hash() { assert_eq!( "c3fcd3d76192e4007dfb496cca67e13b", md5("abcdefghijklmnopqrstuvwxyz") - ) + ); } #[test] diff --git a/soulseek-rs-lib/src/utils/path.rs b/soulseek-rs-lib/src/utils/path.rs index baf073d..b1d261d 100644 --- a/soulseek-rs-lib/src/utils/path.rs +++ b/soulseek-rs-lib/src/utils/path.rs @@ -1,5 +1,6 @@ use std::path::PathBuf; +#[must_use] pub fn expand_tilde(path: &str) -> PathBuf { if let Some(stripped) = path.strip_prefix('~') && let Ok(home) = std::env::var("HOME") diff --git a/soulseek-rs-lib/src/utils/thread_pool.rs b/soulseek-rs-lib/src/utils/thread_pool.rs index 371c5bc..20fe59f 100644 --- a/soulseek-rs-lib/src/utils/thread_pool.rs +++ b/soulseek-rs-lib/src/utils/thread_pool.rs @@ -16,7 +16,8 @@ pub struct ThreadPool { } impl ThreadPool { - pub fn new(size: usize) -> ThreadPool { + #[must_use] + pub fn new(size: usize) -> Self { assert!(size > 0); let (sender, receiver) = mpsc::channel(); @@ -27,7 +28,7 @@ impl ThreadPool { workers.push(Worker::new(Arc::clone(&receiver))); } - ThreadPool { + Self { workers, sender: Some(sender), } @@ -62,6 +63,39 @@ impl Drop for ThreadPool { } } +struct Worker { + thread: Option>, +} + +impl Worker { + fn new(receiver: Arc>>) -> Self { + let thread = thread::spawn(move || { + loop { + let message = match receiver.lock_safe() { + Ok(rx) => rx.recv(), + Err(_) => break, + }; + match message { + // Contain a panicking job so it kills only that job, not the + // worker: the pool has a fixed number of workers and a lost + // one is never replaced. The lock is already released here, + // so a panic cannot poison the shared receiver. + Ok(Message::NewJob(job)) => { + let _ = std::panic::catch_unwind( + std::panic::AssertUnwindSafe(job), + ); + } + Ok(Message::Terminate) | Err(_) => break, + } + } + }); + + Self { + thread: Some(thread), + } + } +} + #[cfg(test)] mod tests { use super::ThreadPool; @@ -88,36 +122,3 @@ mod tests { ); } } - -struct Worker { - thread: Option>, -} - -impl Worker { - fn new(receiver: Arc>>) -> Worker { - let thread = thread::spawn(move || { - loop { - let message = match receiver.lock_safe() { - Ok(rx) => rx.recv(), - Err(_) => break, - }; - match message { - // Contain a panicking job so it kills only that job, not the - // worker: the pool has a fixed number of workers and a lost - // one is never replaced. The lock is already released here, - // so a panic cannot poison the shared receiver. - Ok(Message::NewJob(job)) => { - let _ = std::panic::catch_unwind( - std::panic::AssertUnwindSafe(job), - ); - } - Ok(Message::Terminate) | Err(_) => break, - } - } - }); - - Worker { - thread: Some(thread), - } - } -} diff --git a/soulseek-rs-lib/src/utils/zlib.rs b/soulseek-rs-lib/src/utils/zlib.rs index 3c20d63..4ba7ef4 100644 --- a/soulseek-rs-lib/src/utils/zlib.rs +++ b/soulseek-rs-lib/src/utils/zlib.rs @@ -8,7 +8,7 @@ struct BitReader { } impl BitReader { - fn new(mem: Vec) -> Self { + const fn new(mem: Vec) -> Self { Self { mem, pos: 0, @@ -42,7 +42,7 @@ impl BitReader { fn read_bits(&mut self, n: usize) -> std::result::Result { let mut o = 0u32; for i in 0..n { - o |= (self.read_bit()? as u32) << i; + o |= u32::from(self.read_bit()?) << i; } Ok(o) } @@ -51,7 +51,7 @@ impl BitReader { // read bytes as an integer in little-endian let mut o = 0u32; for i in 0..n { - o |= (self.read_byte()? as u32) << (8 * i); + o |= u32::from(self.read_byte()?) << (8 * i); } Ok(o) } @@ -72,7 +72,7 @@ pub fn deflate(input: &[u8]) -> Result> { return Err(SoulseekRs::CompressionError("invalid CINFO".to_string())); } let flg = r.read_byte()?; - if !((cmf as u32) * 256 + (flg as u32)).is_multiple_of(31) { + if !(u32::from(cmf) * 256 + u32::from(flg)).is_multiple_of(31) { return Err(SoulseekRs::CompressionError( "CMF+FLG checksum failed".to_string(), )); @@ -119,12 +119,12 @@ fn inflate_block_no_compression( #[derive(Clone)] struct Node { symbol: Option, - left: Option>, - right: Option>, + left: Option>, + right: Option>, } impl Node { - fn new() -> Self { + const fn new() -> Self { Self { symbol: None, left: None, @@ -138,7 +138,7 @@ struct HuffmanTree { } impl HuffmanTree { - fn new() -> Self { + const fn new() -> Self { Self { root: Node::new() } } @@ -175,9 +175,9 @@ fn decode_symbol( // at a missing child; error out instead of unwrap-panicking. node = next .as_ref() - .ok_or("Invalid Huffman code".to_string())?; + .ok_or_else(|| "Invalid Huffman code".to_string())?; } - node.symbol.ok_or("No symbol found".to_string()) + node.symbol.ok_or_else(|| "No symbol found".to_string()) } const LENGTH_EXTRA_BITS: [usize; 29] = [ @@ -397,7 +397,7 @@ mod tests { #[test] fn test_extract_header_success() { - let _data = [120, 156]; // Valid zlib header + // [120, 156] is a valid zlib header let result = deflate(&[120, 156, 3, 0, 0, 0, 0, 1]); // Minimal valid zlib stream assert!(result.is_ok()); } diff --git a/soulseek-rs/Cargo.toml b/soulseek-rs/Cargo.toml index 00d3907..df2f614 100644 --- a/soulseek-rs/Cargo.toml +++ b/soulseek-rs/Cargo.toml @@ -13,6 +13,9 @@ categories = ["command-line-utilities"] name = "soulseek-rs" path = "src/main.rs" +[lints] +workspace = true + [dependencies] soulseek-rs-lib = { version = "0.3.0", path = "../soulseek-rs-lib" } diff --git a/soulseek-rs/src/main.rs b/soulseek-rs/src/main.rs index 4aea41d..6e4abf3 100644 --- a/soulseek-rs/src/main.rs +++ b/soulseek-rs/src/main.rs @@ -35,7 +35,7 @@ fn main() -> Result<()> { if let Some(log_file) = &cli.log_file { // SAFETY: Called before any threads are spawned unsafe { - env::set_var("LOG_FILE", log_file.to_string_lossy().to_string()) + env::set_var("LOG_FILE", log_file.to_string_lossy().to_string()); }; } @@ -76,17 +76,20 @@ fn main() -> Result<()> { search_and_download(config) } None => { + use ratatui::crossterm::{ + event::EnableMouseCapture, + execute, + terminal::{Clear, ClearType}, + }; + // Launch main TUI // Enable logger buffering BEFORE connection to prevent log artifacts soulseek_rs::utils::logger::enable_buffering(); let settings = ClientSettings { - username: username.clone(), - password: password.clone(), - server_address: PeerAddress::new( - server_host.clone(), - server_port, - ), + username, + password, + server_address: PeerAddress::new(server_host, server_port), enable_listen: !cli.disable_listener, listen_port: cli.listener_port, }; @@ -102,11 +105,6 @@ fn main() -> Result<()> { let client = Arc::new(client); // Clear screen and enable mouse capture before initializing TUI - use ratatui::crossterm::{ - event::EnableMouseCapture, - execute, - terminal::{Clear, ClearType}, - }; let _ = execute!( std::io::stdout(), Clear(ClearType::All), diff --git a/soulseek-rs/src/models/app_state.rs b/soulseek-rs/src/models/app_state.rs index e9e6a58..a8fa55c 100644 --- a/soulseek-rs/src/models/app_state.rs +++ b/soulseek-rs/src/models/app_state.rs @@ -5,7 +5,7 @@ use std::sync::atomic::AtomicBool; use std::sync::{Arc, mpsc::Receiver, mpsc::Sender}; use std::time::Instant; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum SearchStatus { Active, Completed, @@ -25,7 +25,7 @@ pub struct DownloadEntry { pub receiver: Option>, } -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FocusedPane { Searches, Results, diff --git a/soulseek-rs/src/ui/download_selector.rs b/soulseek-rs/src/ui/download_selector.rs index 0c9f144..49adea6 100644 --- a/soulseek-rs/src/ui/download_selector.rs +++ b/soulseek-rs/src/ui/download_selector.rs @@ -255,7 +255,10 @@ impl FileSelector { } /// Previous index with wrap-around, or None when the list is empty. - fn wrapping_prev(selected: Option, len: usize) -> Option { + const fn wrapping_prev( + selected: Option, + len: usize, + ) -> Option { if len == 0 { return None; } @@ -266,7 +269,10 @@ impl FileSelector { } /// Next index with wrap-around, or None when the list is empty. - fn wrapping_next(selected: Option, len: usize) -> Option { + const fn wrapping_next( + selected: Option, + len: usize, + ) -> Option { if len == 0 { return None; } @@ -276,12 +282,12 @@ impl FileSelector { }) } - fn select_previous(&mut self) { + const fn select_previous(&mut self) { let i = Self::wrapping_prev(self.state.selected(), self.items.len()); self.state.select(i); } - fn select_next(&mut self) { + const fn select_next(&mut self) { let i = Self::wrapping_next(self.state.selected(), self.items.len()); self.state.select(i); } @@ -290,7 +296,7 @@ impl FileSelector { self.state.select((!self.items.is_empty()).then_some(0)); } - fn select_last(&mut self) { + const fn select_last(&mut self) { self.state.select(self.items.len().checked_sub(1)); } @@ -395,9 +401,9 @@ impl FileSelector { let is_selected = self.selected_indices.contains(&original_idx); let checkbox = if is_selected { "[✓]" } else { "[ ]" }; - let speed_mb = item.speed as f64 / BYTES_PER_MB; + let speed_mb = f64::from(item.speed) / BYTES_PER_MB; let speed_str = if speed_mb > 0.0 { - format!("{:.1} MB/s", speed_mb) + format!("{speed_mb:.1} MB/s") } else { "-".to_string() }; @@ -405,7 +411,7 @@ impl FileSelector { let slots_str = format!("{}", item.slots); let bitrate_str = match item.bitrate { - Some(br) => format!("{} kbps", br), + Some(br) => format!("{br} kbps"), None => "-".to_string(), }; @@ -474,7 +480,7 @@ impl FileSelector { self.search_start_time.elapsed().as_secs(), self.search_timeout.as_secs() )), - ]) + ]); } else { loading_message.extend(vec![ Span::raw(spinner), @@ -482,7 +488,7 @@ impl FileSelector { Span::styled(self.soulseek_query.clone(), primary_style()), Span::raw("'; No results yet"), ]); - }; + } // Center the loading message let vertical = Layout::vertical([ diff --git a/soulseek-rs/src/ui/downloads.rs b/soulseek-rs/src/ui/downloads.rs index 6c10cc4..b53316c 100644 --- a/soulseek-rs/src/ui/downloads.rs +++ b/soulseek-rs/src/ui/downloads.rs @@ -155,7 +155,7 @@ impl MultiDownloadProgress { } } - fn select_previous(&mut self) { + const fn select_previous(&mut self) { let i = match self.list_state.selected() { Some(i) if i > 0 => i - 1, _ => self.downloads.len().saturating_sub(1), @@ -163,7 +163,7 @@ impl MultiDownloadProgress { self.list_state.select(Some(i)); } - fn select_next(&mut self) { + const fn select_next(&mut self) { let i = match self.list_state.selected() { Some(i) if i < self.downloads.len().saturating_sub(1) => i + 1, _ => 0, @@ -171,11 +171,11 @@ impl MultiDownloadProgress { self.list_state.select(Some(i)); } - fn select_first(&mut self) { + const fn select_first(&mut self) { self.list_state.select(Some(0)); } - fn select_last(&mut self) { + const fn select_last(&mut self) { if !self.downloads.is_empty() { self.list_state.select(Some(self.downloads.len() - 1)); } @@ -198,7 +198,7 @@ impl MultiDownloadProgress { self.render_downloads_list(frame, chunks[1]); // Bottom: Controls - self.render_controls(frame, chunks[2]); + Self::render_controls(frame, chunks[2]); } fn render_stats(&self, frame: &mut Frame, area: ratatui::layout::Rect) { @@ -312,7 +312,7 @@ impl MultiDownloadProgress { ); } - fn render_controls(&self, frame: &mut Frame, area: ratatui::layout::Rect) { + fn render_controls(frame: &mut Frame, area: ratatui::layout::Rect) { let controls_line = format_shortcuts_styled(&[ ("↑↓/jk", "scroll"), ("Home/End", "jump"), @@ -402,7 +402,7 @@ pub fn render_download_stats( let stats_line = Line::from(vec![ Span::raw("soulseek-rs 🦀 "), Span::styled( - format!("v{} ", VERSION), + format!("v{VERSION} "), Style::default() .fg(COLOR_PRIMARY) .add_modifier(Modifier::BOLD), @@ -460,7 +460,7 @@ pub fn render_download_stats( spans.extend(data_str.spans); spans.push(Span::raw(" • ")); spans.push(Span::styled( - format!("{}", speed_mb), + format!("{speed_mb}"), Style::default() .fg(COLOR_PRIMARY) .add_modifier(Modifier::BOLD), @@ -487,9 +487,9 @@ pub fn show_multi_download_progress( let (tx, rx) = mpsc::channel(); // Spawn background thread to initialize downloads - let init_client = client.clone(); + let init_client = client; thread::spawn(move || { - for (filename, username, size) in selected_files.into_iter() { + for (filename, username, size) in selected_files { // Initiate download match init_client.download( filename.clone(), @@ -501,10 +501,7 @@ pub fn show_multi_download_progress( let _ = tx.send((download, receiver)); } Err(e) => { - eprintln!( - "Failed to start download for {}: {}", - filename, e - ); + eprintln!("Failed to start download for {filename}: {e}"); } } } diff --git a/soulseek-rs/src/ui/main_tui.rs b/soulseek-rs/src/ui/main_tui.rs index a40326d..d182bcc 100644 --- a/soulseek-rs/src/ui/main_tui.rs +++ b/soulseek-rs/src/ui/main_tui.rs @@ -61,13 +61,14 @@ impl MainTui { } pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { + use ratatui::crossterm::{event::DisableMouseCapture, execute}; + // Run the event loop, then restore the terminal unconditionally: if the // loop returns early with an error the terminal must still be taken out // of raw mode / the alternate screen and mouse capture disabled, or the // user is left with a corrupted terminal. let result = self.run_event_loop(&mut terminal); - use ratatui::crossterm::{event::DisableMouseCapture, execute}; let _ = execute!(std::io::stdout(), DisableMouseCapture); ratatui::restore(); soulseek_rs::utils::logger::disable_buffering(); @@ -75,10 +76,7 @@ impl MainTui { result } - fn run_event_loop( - &mut self, - terminal: &mut DefaultTerminal, - ) -> Result<()> { + fn run_event_loop(&mut self, terminal: &mut DefaultTerminal) -> Result<()> { while !self.state.should_exit { terminal.draw(|frame| self.render(frame))?; @@ -95,10 +93,10 @@ impl MainTui { if poll(Duration::from_millis(100))? { match event::read()? { Event::Key(key) if key.kind == KeyEventKind::Press => { - self.handle_key_event(key)?; + self.handle_key_event(key); } Event::Mouse(mouse) => { - self.handle_mouse_event(mouse)?; + self.handle_mouse_event(mouse); } _ => {} } @@ -328,7 +326,7 @@ impl MainTui { } } - fn handle_key_event(&mut self, key: KeyEvent) -> Result<()> { + fn handle_key_event(&mut self, key: KeyEvent) { // Command bar takes priority if self.state.command_bar_active { return self.handle_command_bar_input(key); @@ -345,25 +343,25 @@ impl MainTui { match key.code { KeyCode::Char('q') => { self.state.should_exit = true; - return Ok(()); + return; } KeyCode::Char('1') => { self.state.focused_pane = FocusedPane::Searches; - return Ok(()); + return; } KeyCode::Char('2') => { self.state.focused_pane = FocusedPane::Results; - return Ok(()); + return; } KeyCode::Char('3') => { self.state.focused_pane = FocusedPane::Downloads; - return Ok(()); + return; } KeyCode::Char('s') => { self.state.command_bar_active = true; self.state.command_bar_input.clear(); self.state.command_bar_cursor_position = 0; - return Ok(()); + return; } _ => {} } @@ -376,7 +374,7 @@ impl MainTui { } } - fn handle_command_bar_input(&mut self, key: KeyEvent) -> Result<()> { + fn handle_command_bar_input(&mut self, key: KeyEvent) { self.state.command_bar_cursor_position = clamp_cursor_to_char_boundary( &self.state.command_bar_input, self.state.command_bar_cursor_position, @@ -474,10 +472,9 @@ impl MainTui { } _ => {} } - Ok(()) } - fn handle_filter_input(&mut self, key: KeyEvent) -> Result<()> { + fn handle_filter_input(&mut self, key: KeyEvent) { match key.code { KeyCode::Esc => { self.state.results_is_filtering = false; @@ -497,10 +494,9 @@ impl MainTui { } _ => {} } - Ok(()) } - fn handle_searches_input(&mut self, key: KeyEvent) -> Result<()> { + fn handle_searches_input(&mut self, key: KeyEvent) { match key.code { KeyCode::Up | KeyCode::Char('k') if !self.state.searches.is_empty() => @@ -551,10 +547,9 @@ impl MainTui { } _ => {} } - Ok(()) } - fn handle_results_input(&mut self, key: KeyEvent) -> Result<()> { + fn handle_results_input(&mut self, key: KeyEvent) { let items_count = if self.state.results_filter_query.is_empty() { self.state.results_items.len() } else { @@ -624,10 +619,9 @@ impl MainTui { } _ => {} } - Ok(()) } - fn handle_downloads_input(&mut self, key: KeyEvent) -> Result<()> { + fn handle_downloads_input(&mut self, key: KeyEvent) { match key.code { KeyCode::Up | KeyCode::Char('k') if !self.state.downloads.is_empty() => @@ -657,10 +651,9 @@ impl MainTui { } _ => {} } - Ok(()) } - fn toggle_selected_download_pause(&mut self) { + fn toggle_selected_download_pause(&self) { let Some(index) = self.state.downloads_table_state.selected() else { return; }; @@ -671,11 +664,13 @@ impl MainTui { let download = &download_entry.download; match download.status { DownloadStatus::InProgress { .. } => { - self.client + let _ = self + .client .pause_download(&download.username, &download.filename); } DownloadStatus::Paused { .. } => { - self.client + let _ = self + .client .resume_download(&download.username, &download.filename); } _ => {} @@ -716,8 +711,8 @@ impl MainTui { self.state.downloads_table_state.select(Some(next_index)); } - fn handle_mouse_event(&mut self, mouse: MouseEvent) -> Result<()> { - if let MouseEventKind::Down(MouseButton::Left) = mouse.kind { + fn handle_mouse_event(&mut self, mouse: MouseEvent) { + if mouse.kind == MouseEventKind::Down(MouseButton::Left) { let (col, row) = (mouse.column, mouse.row); // Check if click is within searches pane @@ -728,7 +723,7 @@ impl MainTui { && row < area.y + area.height { self.state.focused_pane = FocusedPane::Searches; - return Ok(()); + return; } // Check if click is within results pane @@ -739,7 +734,7 @@ impl MainTui { && row < area.y + area.height { self.state.focused_pane = FocusedPane::Results; - return Ok(()); + return; } // Check if click is within downloads pane @@ -750,11 +745,8 @@ impl MainTui { && row < area.y + area.height { self.state.focused_pane = FocusedPane::Downloads; - return Ok(()); } } - - Ok(()) } fn remove_search_at_index(&mut self, index: usize) { @@ -889,7 +881,7 @@ impl MainTui { // Results will be polled in update_search_results } Err(e) => { - eprintln!("Search failed: {}", e); + eprintln!("Search failed: {e}"); } } }); @@ -899,19 +891,14 @@ impl MainTui { let timeout = self.search_timeout; let selected_search_index = self.state.selected_search_index; - // Collect all queries first - let queries: Vec<(usize, String)> = self + // Fetch all results in one go (single lock acquisition per query) + // Use try_get_search_results to avoid blocking the UI thread + let all_results: Vec<(usize, Vec<_>)> = self .state .searches .iter() .enumerate() .map(|(idx, s)| (idx, s.query.clone())) - .collect(); - - // Fetch all results in one go (single lock acquisition per query) - // Use try_get_search_results to avoid blocking the UI thread - let all_results: Vec<(usize, Vec<_>)> = queries - .into_iter() .filter_map(|(idx, query)| { self.client .try_get_search_results(&query) @@ -989,7 +976,7 @@ impl MainTui { let sender = self.state.downloads_sender_channel.clone().unwrap(); thread::spawn(move || { - for file in selected_files.into_iter() { + for file in selected_files { let metadata = soulseek_rs::types::DownloadMetadata { bitrate: file.bitrate, length_seconds: file.length_seconds, @@ -1066,7 +1053,10 @@ pub fn launch_main_tui( tui.run(terminal) } -fn clamp_cursor_to_char_boundary(input: &str, cursor_position: usize) -> usize { +const fn clamp_cursor_to_char_boundary( + input: &str, + cursor_position: usize, +) -> usize { if cursor_position >= input.len() { return input.len(); } @@ -1084,8 +1074,7 @@ fn previous_char_boundary(input: &str, cursor_position: usize) -> usize { input[..cursor_position] .char_indices() .last() - .map(|(index, _)| index) - .unwrap_or(0) + .map_or(0, |(index, _)| index) } fn next_char_boundary(input: &str, cursor_position: usize) -> usize { @@ -1099,8 +1088,7 @@ fn next_char_boundary(input: &str, cursor_position: usize) -> usize { + input[cursor_position..] .chars() .next() - .map(char::len_utf8) - .unwrap_or(0) + .map_or(0, char::len_utf8) } fn visible_input_at_cursor( diff --git a/soulseek-rs/src/ui/panes/download_info_pane.rs b/soulseek-rs/src/ui/panes/download_info_pane.rs index 04fd9c1..30063c1 100644 --- a/soulseek-rs/src/ui/panes/download_info_pane.rs +++ b/soulseek-rs/src/ui/panes/download_info_pane.rs @@ -83,7 +83,7 @@ fn build_info_lines( lines.push(label_value("Save to", &save_path)); if let Some(bitrate) = download.metadata.bitrate { - lines.push(label_value("Bitrate", &format!("{} kbps", bitrate))); + lines.push(label_value("Bitrate", &format!("{bitrate} kbps"))); } if let Some(length) = download.metadata.length_seconds { lines.push(label_value("Length", &format_duration(length))); @@ -93,14 +93,14 @@ fn build_info_lines( DownloadStatus::Queued => { lines.push(Line::from("")); let position_text = match download.queue_position { - Some(p) => format!("#{}", p), + Some(p) => format!("#{p}"), None => "unknown".to_string(), }; lines.push(label_value("Queue position", &position_text)); if let Some(slots) = download.metadata.peer_free_slots { let slot_text = if slots > 0 { - format!("{} available", slots) + format!("{slots} available") } else { "all busy".to_string() }; @@ -110,7 +110,7 @@ fn build_info_lines( if let Some(speed) = download.metadata.peer_upload_speed { lines.push(label_value( "Peer upload speed", - &format_speed(speed as f64), + &format_speed(f64::from(speed)), )); } } @@ -149,10 +149,7 @@ fn build_info_lines( fn label_value(label: &str, value: &str) -> Line<'static> { Line::from(vec![ - Span::styled( - format!("{: Line<'static> { Line::from(vec![ - Span::styled( - format!("{: String { let minutes = (seconds % 3600) / 60; let secs = seconds % 60; if hours > 0 { - format!("{}h {:02}m {:02}s", hours, minutes, secs) + format!("{hours}h {minutes:02}m {secs:02}s") } else if minutes > 0 { - format!("{}m {:02}s", minutes, secs) + format!("{minutes}m {secs:02}s") } else { - format!("{}s", secs) + format!("{secs}s") } } diff --git a/soulseek-rs/src/ui/panes/results_pane.rs b/soulseek-rs/src/ui/panes/results_pane.rs index ed80b8e..757b16c 100644 --- a/soulseek-rs/src/ui/panes/results_pane.rs +++ b/soulseek-rs/src/ui/panes/results_pane.rs @@ -63,7 +63,7 @@ pub fn render_results_pane( } = params; if items.is_empty() { let title = if let Some(query) = active_search_query { - format!("[2] Results: {}", query) + format!("[2] Results: {query}") } else { "[2] Results".to_string() }; @@ -75,13 +75,12 @@ pub fn render_results_pane( .title(title); let message = if is_filtering { - format!("No results match filter: '{}'", filter_query) + format!("No results match filter: '{filter_query}'") } else { format!( - "soulseek-rs 🦀 v{} + "soulseek-rs 🦀 v{VERSION} Michel de Graaf 2026\n -No results. Select a search from the Searches pane [1]. Or start new search [s → search] \n", - VERSION +No results. Select a search from the Searches pane [1]. Or start new search [s → search] \n" ) }; @@ -114,13 +113,13 @@ No results. Select a search from the Searches pane [1]. Or start new search [s let bitrate_str = file .bitrate - .map(|br| format!("{} kbps", br)) - .unwrap_or_else(|| "-".to_string()); + .map_or_else(|| "-".to_string(), |br| format!("{br} kbps")); let speed_str = if file.speed > 0 { - let speed_mb = - (file.speed as f64 / BYTES_PER_MB * 100.0).round() / 100.0; - format!("{} MB/s", speed_mb) + let speed_mb = (f64::from(file.speed) / BYTES_PER_MB * 100.0) + .round() + / 100.0; + format!("{speed_mb} MB/s") } else { "-".to_string() }; @@ -148,9 +147,9 @@ No results. Select a search from the Searches pane [1]. Or start new search [s ]; let title = if is_filtering { - format!("[2] Results - Filter: '{}'", filter_query) + format!("[2] Results - Filter: '{filter_query}'") } else if let Some(query) = active_search_query { - format!("[2] Results: {}", query) + format!("[2] Results: {query}") } else { "[2] Results".to_string() }; @@ -178,7 +177,7 @@ mod tests { #[test] fn identity_mapping_used_when_no_filter() { - let selected: HashSet = [1].into_iter().collect(); + let selected: HashSet = std::iter::once(1).collect(); assert!(row_is_selected(1, None, &selected)); assert!(!row_is_selected(0, None, &selected)); } @@ -188,7 +187,7 @@ mod tests { // Filter shows original rows 2, 5, 7 as display rows 0, 1, 2. // The user selected original row 5. let original_indices = [2usize, 5, 7]; - let selected: HashSet = [5].into_iter().collect(); + let selected: HashSet = std::iter::once(5).collect(); assert!(!row_is_selected(0, Some(&original_indices), &selected)); assert!(row_is_selected(1, Some(&original_indices), &selected)); assert!(!row_is_selected(2, Some(&original_indices), &selected)); diff --git a/soulseek-rs/src/ui/panes/searches_pane.rs b/soulseek-rs/src/ui/panes/searches_pane.rs index de24644..3fbddc1 100644 --- a/soulseek-rs/src/ui/panes/searches_pane.rs +++ b/soulseek-rs/src/ui/panes/searches_pane.rs @@ -41,7 +41,7 @@ pub fn render_searches_pane( { "Searching...".to_string() } else { - format!("{}", results_count) + format!("{results_count}") }; Row::new(vec![ diff --git a/soulseek-rs/src/ui/styles.rs b/soulseek-rs/src/ui/styles.rs index 9171547..505c2f7 100644 --- a/soulseek-rs/src/ui/styles.rs +++ b/soulseek-rs/src/ui/styles.rs @@ -71,11 +71,11 @@ pub fn unfocused_border_style() -> Style { Style::default() } -pub fn focused_border_type() -> BorderType { +pub const fn focused_border_type() -> BorderType { BorderType::Rounded } -pub fn unfocused_border_type() -> BorderType { +pub const fn unfocused_border_type() -> BorderType { BorderType::Rounded } @@ -88,7 +88,7 @@ pub fn border_style(focused: bool) -> Style { } } -pub fn border_type(focused: bool) -> BorderType { +pub const fn border_type(focused: bool) -> BorderType { if focused { focused_border_type() } else { @@ -100,9 +100,7 @@ pub fn border_type(focused: bool) -> BorderType { pub fn format_shortcuts(shortcuts: &[(&str, &str)]) -> String { shortcuts .iter() - .map(|(key, action)| { - format!("[{} {} {}]", key, HIGHLIGHT_SYMBOL, action) - }) + .map(|(key, action)| format!("[{key} {HIGHLIGHT_SYMBOL} {action}]")) .collect::>() .join(" ") } @@ -124,7 +122,7 @@ pub fn format_shortcuts_styled(shortcuts: &[(&str, &str)]) -> Line<'static> { .add_modifier(Modifier::BOLD), )); spans.push(Span::styled( - format!(" {} ", HIGHLIGHT_SYMBOL), + format!(" {HIGHLIGHT_SYMBOL} "), dimmed_style(), )); spans.push(Span::raw(action.to_string())); @@ -165,7 +163,7 @@ pub fn format_progress_bar( Span::raw("]"), Span::raw(" "), Span::styled( - format!("{}%", percentage), + format!("{percentage}%"), primary_style() .add_modifier(Modifier::BOLD) .fg(Color::default()), diff --git a/soulseek-rs/src/ui/utils.rs b/soulseek-rs/src/ui/utils.rs index bf75a0f..247d1b0 100644 --- a/soulseek-rs/src/ui/utils.rs +++ b/soulseek-rs/src/ui/utils.rs @@ -39,6 +39,6 @@ pub fn get_bitrate( const SPINNER_CHARS: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; -pub fn get_spinner_char(state: usize) -> &'static str { +pub const fn get_spinner_char(state: usize) -> &'static str { SPINNER_CHARS[state % SPINNER_CHARS.len()] }