use std::{fmt::Write, time::Instant};
use parking_lot::RwLock;
pub const DEFAULT_CLIENT_CAPACITY: usize = 8 * 1024;
pub const MAX_DETAILS_LEN: usize = 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClientEventType {
KeyPress,
CommandExecuted,
ModeChanged,
StateChanged,
Error,
Warning,
Info,
}
impl ClientEventType {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::KeyPress => "KEY",
Self::CommandExecuted => "CMD",
Self::ModeChanged => "MODE",
Self::StateChanged => "STATE",
Self::Error => "ERROR",
Self::Warning => "WARN",
Self::Info => "INFO",
}
}
}
impl std::fmt::Display for ClientEventType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct ClientLogEntry {
pub seq: u64,
pub timestamp_us: u64,
pub event_type: ClientEventType,
pub details: String,
}
impl ClientLogEntry {
#[allow(clippy::missing_const_for_fn)]
fn size_bytes(&self) -> usize {
17 + self.details.capacity()
}
}
#[derive(Debug, Clone, Copy)]
pub struct ClientBufferStats {
pub capacity_bytes: usize,
pub bytes_used: usize,
pub entry_count: usize,
pub total_logged: u64,
pub dropped: u64,
}
#[derive(Debug)]
struct ClientRingBufferInner {
entries: Vec<ClientLogEntry>,
total_logged: u64,
bytes_used: usize,
capacity_bytes: usize,
start_time: Instant,
}
impl ClientRingBufferInner {
fn new(capacity_bytes: usize) -> Self {
Self {
entries: Vec::new(),
total_logged: 0,
bytes_used: 0,
capacity_bytes,
start_time: Instant::now(),
}
}
fn push(&mut self, event_type: ClientEventType, details: String) {
let details = if details.len() > MAX_DETAILS_LEN {
let mut truncated = details[..MAX_DETAILS_LEN].to_string();
truncated.push_str("...");
truncated
} else {
details
};
let entry = ClientLogEntry {
seq: self.total_logged,
#[allow(clippy::cast_possible_truncation)]
timestamp_us: self.start_time.elapsed().as_micros() as u64,
event_type,
details,
};
let entry_size = entry.size_bytes();
self.total_logged += 1;
while self.bytes_used + entry_size > self.capacity_bytes && !self.entries.is_empty() {
let removed = self.entries.remove(0);
self.bytes_used = self.bytes_used.saturating_sub(removed.size_bytes());
}
self.entries.push(entry);
self.bytes_used += entry_size;
}
fn tail(&self, n: usize) -> Vec<ClientLogEntry> {
let count = n.min(self.entries.len());
self.entries.iter().rev().take(count).cloned().collect()
}
fn entries(&self) -> Vec<ClientLogEntry> {
self.entries.clone()
}
fn dump(&self) -> String {
let mut output = String::new();
output.push_str("=== Client Ring Buffer Dump ===\n");
let _ = writeln!(
output,
"Entries: {} | Bytes: {}/{} | Total logged: {}",
self.entries.len(),
self.bytes_used,
self.capacity_bytes,
self.total_logged
);
output.push_str("---\n");
for entry in &self.entries {
let _ = writeln!(
output,
"[{:>10}us] {:5} {}",
entry.timestamp_us,
entry.event_type.as_str(),
entry.details
);
}
output.push_str("=== End Dump ===\n");
output
}
#[allow(clippy::missing_const_for_fn)]
fn stats(&self) -> ClientBufferStats {
let dropped = if self.total_logged > self.entries.len() as u64 {
self.total_logged - self.entries.len() as u64
} else {
0
};
ClientBufferStats {
capacity_bytes: self.capacity_bytes,
bytes_used: self.bytes_used,
entry_count: self.entries.len(),
total_logged: self.total_logged,
dropped,
}
}
}
pub struct ClientRingBuffer {
inner: RwLock<ClientRingBufferInner>,
}
impl ClientRingBuffer {
#[must_use]
pub fn new() -> Self {
Self::with_capacity(DEFAULT_CLIENT_CAPACITY)
}
#[must_use]
pub fn with_capacity(capacity_bytes: usize) -> Self {
Self {
inner: RwLock::new(ClientRingBufferInner::new(capacity_bytes)),
}
}
pub fn log_event(&self, event_type: ClientEventType, details: impl Into<String>) {
if let Some(mut inner) = self.inner.try_write() {
inner.push(event_type, details.into());
}
}
pub fn log_key(&self, key: &str) {
self.log_event(ClientEventType::KeyPress, key);
}
pub fn log_command(&self, command: &str) {
self.log_event(ClientEventType::CommandExecuted, command);
}
pub fn log_mode_change(&self, from: &str, to: &str) {
self.log_event(ClientEventType::ModeChanged, format!("{from} -> {to}"));
}
pub fn log_state_change(&self, description: &str) {
self.log_event(ClientEventType::StateChanged, description);
}
pub fn log_error(&self, error: &str) {
self.log_event(ClientEventType::Error, error);
}
#[must_use]
pub fn tail(&self, n: usize) -> Vec<ClientLogEntry> {
self.inner.read().tail(n)
}
#[must_use]
pub fn entries(&self) -> Vec<ClientLogEntry> {
self.inner.read().entries()
}
#[must_use]
pub fn dump(&self) -> String {
self.inner.read().dump()
}
#[must_use]
pub fn try_dump(&self) -> Option<String> {
self.inner.try_read().map(|inner| inner.dump())
}
#[must_use]
pub fn stats(&self) -> ClientBufferStats {
self.inner.read().stats()
}
#[must_use]
pub fn bytes_used(&self) -> usize {
self.inner.read().bytes_used
}
pub fn clear(&self) {
if let Some(mut inner) = self.inner.try_write() {
inner.entries.clear();
inner.bytes_used = 0;
}
}
}
impl Default for ClientRingBuffer {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for ClientRingBuffer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let stats = self.stats();
f.debug_struct("ClientRingBuffer")
.field("capacity_bytes", &stats.capacity_bytes)
.field("bytes_used", &stats.bytes_used)
.field("entry_count", &stats.entry_count)
.field("total_logged", &stats.total_logged)
.finish()
}
}
impl Clone for ClientRingBuffer {
fn clone(&self) -> Self {
let inner = self.inner.read();
let capacity = inner.capacity_bytes;
let entries = inner.entries.clone();
let total_logged = inner.total_logged;
let bytes_used = inner.bytes_used;
let start_time = inner.start_time;
drop(inner);
let mut new_inner = ClientRingBufferInner::new(capacity);
new_inner.entries = entries;
new_inner.total_logged = total_logged;
new_inner.bytes_used = bytes_used;
new_inner.start_time = start_time;
Self {
inner: RwLock::new(new_inner),
}
}
}
#[cfg(test)]
#[path = "ring_buffer_tests.rs"]
mod tests;