use serde::{Deserialize, Serialize};
const NAME_MAX: usize = 64;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Buffer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub text: String,
pub at: i64,
#[serde(skip)]
pub id: u64,
}
#[derive(Serialize, Deserialize)]
struct Saved {
version: u32,
buffers: Vec<Buffer>,
}
const SAVED_VERSION: u32 = 1;
pub struct Buffers {
list: Vec<Buffer>,
limit: usize,
max_bytes: usize,
next_id: u64,
}
impl Buffers {
pub fn new(limit: usize, max_bytes: usize) -> Buffers {
Buffers { list: Vec::new(), limit, max_bytes, next_id: 1 }
}
pub fn set_limits(&mut self, limit: usize, max_bytes: usize) {
self.limit = limit;
self.max_bytes = max_bytes;
self.trim();
}
pub fn list(&self) -> &[Buffer] {
&self.list
}
pub fn key(&self, index: usize) -> String {
let Some(b) = self.list.get(index) else { return String::new() };
match &b.name {
Some(name) => name.clone(),
None => self.list[..index].iter().filter(|b| b.name.is_none()).count().to_string(),
}
}
pub fn by_id(&self, id: u64) -> Option<&Buffer> {
self.list.iter().find(|b| b.id == id)
}
pub fn key_of(&self, id: u64) -> Option<String> {
self.list.iter().position(|b| b.id == id).map(|i| self.key(i))
}
pub fn copy(&mut self, text: &str, at: i64) -> bool {
if text.is_empty() || self.limit == 0 || text.len() > self.max_bytes {
return false;
}
if let Some(i) = self.list.iter().position(|b| b.name.is_none())
&& self.list[i].text == text
{
let mut b = self.list.remove(i);
b.at = at;
self.list.insert(0, b);
return true;
}
self.push(None, text.to_string(), at);
true
}
pub fn set(&mut self, name: Option<&str>, text: String, at: i64) -> Result<(), String> {
let most = crate::text::human_size(self.max_bytes as u64);
if text.len() > self.max_bytes {
return Err(format!("larger than buffers.max_mb ({most})"));
}
let Some(name) = name else {
if self.limit == 0 {
return Err("buffers.limit is 0: no numbered buffer is kept (give it a name)".into());
}
self.copy(&text, at);
return Ok(());
};
check_name(name)?;
let named: usize =
self.list.iter().filter(|b| b.name.as_deref().is_some_and(|n| n != name)).map(|b| b.text.len()).sum();
if named + text.len() > self.max_bytes {
return Err(format!("named buffers would hold more than buffers.max_mb ({most}): delete some first"));
}
self.list.retain(|b| b.name.as_deref() != Some(name));
self.push(Some(name.to_string()), text, at);
Ok(())
}
fn push(&mut self, name: Option<String>, text: String, at: i64) {
let id = self.next_id;
self.next_id += 1;
self.list.insert(0, Buffer { name, text, at, id });
self.trim();
}
fn trim(&mut self) {
let mut numbered = self.list.iter().filter(|b| b.name.is_none()).count();
let mut bytes: usize = self.list.iter().map(|b| b.text.len()).sum();
while numbered > 0 && (numbered > self.limit || bytes > self.max_bytes) {
let Some(i) = self.list.iter().rposition(|b| b.name.is_none()) else { break };
bytes -= self.list.remove(i).text.len();
numbered -= 1;
}
}
pub fn get(&self, key: Option<&str>) -> Result<&Buffer, String> {
self.index(key).map(|i| &self.list[i])
}
fn index(&self, key: Option<&str>) -> Result<usize, String> {
let Some(key) = key else {
return if self.list.is_empty() { Err("no paste buffers yet".into()) } else { Ok(0) };
};
if !is_number(key) {
let named = self.list.iter().position(|b| b.name.as_deref() == Some(key));
return named.ok_or(format!("no buffer named {key:?}"));
}
let numbered: Vec<usize> = (0..self.list.len()).filter(|i| self.list[*i].name.is_none()).collect();
key.parse::<usize>().ok().and_then(|n| numbered.get(n).copied()).ok_or_else(|| match numbered.len() {
0 => format!("no buffer {key}: there is no numbered buffer"),
1 => format!("no buffer {key}: there is one numbered buffer, 0"),
count => format!("no buffer {key}: numbered buffers go from 0 (the newest) to {}", count - 1),
})
}
pub fn delete(&mut self, key: Option<&str>) -> Result<Buffer, String> {
let i = self.index(key)?;
Ok(self.list.remove(i))
}
pub fn delete_id(&mut self, id: u64) -> Option<Buffer> {
let i = self.list.iter().position(|b| b.id == id)?;
Some(self.list.remove(i))
}
pub fn clear(&mut self) -> usize {
std::mem::take(&mut self.list).len()
}
pub fn to_json(&self) -> Vec<u8> {
let saved = Saved { version: SAVED_VERSION, buffers: self.list.clone() };
serde_json::to_vec(&saved).unwrap_or_default()
}
pub fn load_json(&mut self, data: &[u8]) -> Result<(), String> {
let saved: Saved = serde_json::from_slice(data).map_err(|e| e.to_string())?;
if saved.version != SAVED_VERSION {
return Err(format!("unknown version {}", saved.version));
}
let mut list: Vec<Buffer> = Vec::new();
for mut b in saved.buffers {
if b.name.as_deref().is_some_and(|n| check_name(n).is_err() || list.iter().any(|o| o.name == b.name)) {
continue;
}
b.id = self.next_id;
self.next_id += 1;
list.push(b);
}
self.list = list;
self.trim();
Ok(())
}
}
pub fn check_name(name: &str) -> Result<(), String> {
if name.is_empty() || name.chars().count() > NAME_MAX {
return Err(format!("a buffer name has 1 to {NAME_MAX} characters"));
}
if is_number(name) {
return Err(format!("{name:?} is a number: numbers name the numbered buffers"));
}
if !name.chars().all(|c| c.is_alphanumeric() || matches!(c, '-' | '_' | '.' | ':' | '@' | '+')) {
return Err(format!("{name:?}: a buffer name has letters, digits and - _ . : @ + only"));
}
Ok(())
}
fn is_number(key: &str) -> bool {
!key.is_empty() && key.bytes().all(|b| b.is_ascii_digit())
}
pub fn one_line(text: &str, sep: &str, width: usize) -> String {
let mut line = String::new();
for (i, l) in text.trim().lines().enumerate() {
if i > 0 {
line.push_str(sep);
}
line.push_str(&crate::text::sanitize(l.trim_end()));
if crate::text::columns(&line) > width {
break;
}
}
crate::text::truncate(&line, width)
}
pub fn age(secs: i64) -> String {
let s = secs.max(0);
match s {
0..60 => format!("{s}s"),
60..3600 => format!("{}m", s / 60),
3600..86400 => format!("{}h", s / 3600),
_ => format!("{}d", s / 86400),
}
}
pub fn lines(text: &str) -> usize {
text.lines().count()
}
#[cfg(test)]
mod tests {
use super::*;
fn texts(b: &Buffers) -> Vec<(String, &str)> {
(0..b.list().len()).map(|i| (b.key(i), b.list()[i].text.as_str())).collect()
}
#[test]
fn copies_rotate_within_the_count_and_the_size() {
let mut b = Buffers::new(3, 100);
for (i, t) in ["one", "two", "three", "four"].iter().enumerate() {
assert!(b.copy(t, i as i64));
}
assert_eq!(
texts(&b),
[("0".into(), "four"), ("1".into(), "three"), ("2".into(), "two")],
"the oldest went beyond 3"
);
b.copy(&"a".repeat(60), 10);
b.copy(&"b".repeat(50), 11);
assert_eq!(b.list().len(), 1);
assert_eq!(b.list()[0].text, "b".repeat(50));
assert!(!b.copy(&"c".repeat(101), 12));
assert_eq!(b.list().len(), 1);
assert!(!b.copy("", 13));
let mut none = Buffers::new(0, 100);
assert!(!none.copy("x", 0) && none.list().is_empty());
assert!(none.set(None, "x".into(), 0).is_err());
none.set(Some("kept"), "x".into(), 0).unwrap();
assert_eq!(none.list().len(), 1);
let mut b = Buffers::new(5, 100);
for t in ["a", "b", "c", "d"] {
b.copy(t, 0);
}
b.set_limits(2, 100);
assert_eq!(texts(&b), [("0".into(), "d"), ("1".into(), "c")]);
}
#[test]
fn the_same_copy_twice_in_a_row_is_kept_once() {
let mut b = Buffers::new(10, 1000);
b.copy("same", 1);
let id = b.list()[0].id;
assert!(b.copy("same", 5));
assert_eq!(b.list().len(), 1);
assert_eq!((b.list()[0].id, b.list()[0].at), (id, 5), "the same buffer, copied again later");
b.copy("other", 6);
b.copy("same", 7);
assert_eq!(texts(&b), [("0".into(), "same"), ("1".into(), "other"), ("2".into(), "same")]);
b.set(Some("note"), "again".into(), 8).unwrap();
b.copy("again", 9);
assert_eq!(b.list().len(), 5);
b.copy("same", 10);
assert_eq!(b.list().iter().filter(|x| x.text == "same").count(), 3);
}
#[test]
fn named_buffers_stay_and_numbers_count_only_the_numbered() {
let mut b = Buffers::new(2, 1000);
b.copy("first", 1);
b.set(Some("notes"), "my notes".into(), 2).unwrap();
b.copy("second", 3);
b.copy("third", 4);
assert_eq!(texts(&b), [("0".into(), "third"), ("1".into(), "second"), ("notes".into(), "my notes")]);
assert_eq!(b.get(Some("1")).unwrap().text, "second");
assert_eq!(b.get(Some("notes")).unwrap().text, "my notes");
assert_eq!(b.get(None).unwrap().text, "third", "the newest without a name");
assert!(b.get(Some("2")).unwrap_err().contains("0 (the newest) to 1"));
assert!(b.get(Some("nope")).is_err());
b.set(Some("notes"), "new notes".into(), 5).unwrap();
assert_eq!(b.list().iter().filter(|x| x.name.is_some()).count(), 1);
assert_eq!(b.key(0), "notes");
assert_eq!(b.get(None).unwrap().text, "new notes");
for i in 0..10 {
b.copy(&format!("copy {i}"), 10 + i);
}
assert_eq!(b.get(Some("notes")).unwrap().text, "new notes");
assert_eq!(b.list().len(), 3);
for bad in ["12", "", "two words", "a/b", &"x".repeat(65)] {
assert!(b.set(Some(bad), "x".into(), 0).is_err(), "{bad:?}");
}
b.set(Some("ssh-key.v2"), "x".into(), 0).unwrap();
let mut small = Buffers::new(5, 10);
small.copy("12345", 0);
small.set(Some("a"), "1234567".into(), 1).unwrap();
assert_eq!(texts(&small), [("a".into(), "1234567")], "the numbered one made room");
assert!(small.set(Some("b"), "1234".into(), 2).is_err());
small.set(Some("a"), "0123456789".into(), 3).unwrap();
assert!(small.set(None, "0123456789x".into(), 4).is_err());
}
#[test]
fn delete_by_number_name_or_the_newest() {
let mut b = Buffers::new(10, 1000);
b.copy("a", 0);
b.copy("b", 1);
b.set(Some("n"), "named".into(), 2).unwrap();
assert_eq!(b.delete(Some("1")).unwrap().text, "a");
assert_eq!(b.delete(None).unwrap().text, "named");
let id = b.list()[0].id;
assert_eq!(b.key_of(id).as_deref(), Some("0"));
assert!(b.delete_id(id).is_some() && b.list().is_empty());
assert!(b.delete(None).is_err());
b.copy("x", 0);
b.copy("y", 0);
assert_eq!(b.clear(), 2);
}
#[test]
fn a_name_that_parses_as_a_number_names_its_own_buffer() {
let mut b = Buffers::new(10, 1000);
b.copy("numbered", 0);
b.set(Some("+0"), "named".into(), 1).unwrap();
assert_eq!(b.get(Some("+0")).unwrap().text, "named");
assert_eq!(b.delete(Some("+0")).unwrap().text, "named");
assert_eq!(b.get(Some("0")).unwrap().text, "numbered", "the numbered buffer is still there");
assert!(b.get(Some("+0")).unwrap_err().contains("no buffer named"));
assert!(b.get(Some("99999999999999999999999")).unwrap_err().contains("one numbered buffer"));
}
#[test]
fn buffers_kept_across_restarts_come_back_within_the_limits() {
let mut b = Buffers::new(10, 1000);
b.copy("old", 1);
b.set(Some("notes"), "named".into(), 2).unwrap();
b.copy("new", 3);
let json = b.to_json();
let mut again = Buffers::new(1, 1000);
again.load_json(&json).unwrap();
assert_eq!(texts(&again), [("0".into(), "new"), ("notes".into(), "named")]);
assert!(again.list().iter().all(|x| x.id > 0));
assert!(again.load_json(br#"{"version":9,"buffers":[]}"#).is_err());
assert!(again.load_json(b"not json").is_err());
}
#[test]
fn lists_show_one_line_and_an_age() {
assert_eq!(one_line(" first\nsecond\x1b[31m\n", " | ", 40), "first | second\u{241b}[31m");
assert_eq!(one_line(&"x".repeat(50), " | ", 10).chars().count(), 10);
assert_eq!(age(5), "5s");
assert_eq!(age(125), "2m");
assert_eq!(age(7200), "2h");
assert_eq!(age(200_000), "2d");
assert_eq!(age(-3), "0s");
assert_eq!(lines("a\nb\n"), 2);
assert_eq!(lines("a\nb"), 2);
}
}