Skip to main content

rmux_core/
buffers.rs

1//! Server-wide paste buffer store with deterministic naming and FIFO eviction.
2
3use std::collections::BTreeMap;
4
5use chrono::Local;
6use rmux_proto::RmuxError;
7
8use crate::vis::encode_buffer_sample;
9
10/// A single paste buffer entry.
11#[derive(Debug, Clone, PartialEq, Eq)]
12struct BufferEntry {
13    /// The buffer content.
14    content: Vec<u8>,
15    /// Monotonic insertion/replacement order for stack-head tracking.
16    order: u64,
17    /// Creation timestamp matching tmux's `buffer_created`.
18    created: i64,
19    /// Whether this buffer was auto-named (`buffer%d`).
20    unnamed: bool,
21}
22
23/// Server-wide buffer store.
24///
25/// Buffers are server-global state, not session-keyed. Unnamed buffers use
26/// deterministic names (`buffer0`, `buffer1`, …) from a monotonically
27/// increasing sequence. The allocator skips names already occupied by existing
28/// buffers, including explicit named buffers. FIFO eviction at the effective
29/// `buffer-limit` removes only unnamed buffers.
30#[derive(Debug, Clone, Default)]
31pub struct BufferStore {
32    /// Buffers keyed by name.
33    buffers: BTreeMap<String, BufferEntry>,
34    /// Monotonic counter for unnamed buffer names.
35    next_unnamed_id: u32,
36    /// Monotonic counter for insertion/replacement ordering (stack head).
37    next_order: u64,
38}
39
40impl BufferStore {
41    /// Creates an empty buffer store.
42    #[must_use]
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    /// Returns the number of buffers currently stored.
48    #[must_use]
49    pub fn len(&self) -> usize {
50        self.buffers.len()
51    }
52
53    /// Returns whether the store contains no buffers.
54    #[must_use]
55    pub fn is_empty(&self) -> bool {
56        self.buffers.is_empty()
57    }
58
59    /// Returns the name of the stack-head buffer (most recently created/replaced).
60    #[must_use]
61    pub fn stack_head(&self) -> Option<&str> {
62        self.buffers
63            .iter()
64            .max_by_key(|(_, entry)| entry.order)
65            .map(|(name, _)| name.as_str())
66    }
67
68    /// Returns the name of the most recent automatic buffer.
69    #[must_use]
70    pub fn top_unnamed(&self) -> Option<&str> {
71        self.buffers
72            .iter()
73            .filter(|(_, entry)| entry.unnamed)
74            .max_by_key(|(_, entry)| entry.order)
75            .map(|(name, _)| name.as_str())
76    }
77
78    /// Returns the content of the named buffer, or `None` if it does not exist.
79    #[must_use]
80    pub fn get(&self, name: &str) -> Option<&[u8]> {
81        self.buffers.get(name).map(|entry| entry.content.as_slice())
82    }
83
84    /// Sets a named buffer, replacing it if it already exists.
85    ///
86    /// When `name` is `Some`, the buffer is created or replaced with that name.
87    /// When `name` is `None`, a new unnamed buffer is created with the next
88    /// deterministic name.
89    ///
90    /// Zero-length content succeeds without creating or replacing any buffer.
91    ///
92    /// Returns a list of buffer names evicted by FIFO unnamed eviction (may be
93    /// empty), followed by the optional name of the created/replaced buffer.
94    pub fn set(
95        &mut self,
96        name: Option<&str>,
97        content: Vec<u8>,
98        buffer_limit: usize,
99    ) -> Result<SetBufferOutcome, RmuxError> {
100        if let Some(name) = name {
101            validate_buffer_name(name)?;
102        }
103        if content.is_empty() {
104            return Ok(SetBufferOutcome {
105                buffer_name: None,
106                evicted: Vec::new(),
107            });
108        }
109
110        let order = self.next_order;
111        self.next_order += 1;
112        let created = Local::now().timestamp();
113
114        let (buffer_name, is_new_unnamed) = match name {
115            Some(name) => {
116                self.buffers.insert(
117                    name.to_owned(),
118                    BufferEntry {
119                        content,
120                        order,
121                        created,
122                        unnamed: false,
123                    },
124                );
125                (name.to_owned(), false)
126            }
127            None => {
128                let buffer_name = self.allocate_unnamed_name()?;
129                self.buffers.insert(
130                    buffer_name.clone(),
131                    BufferEntry {
132                        content,
133                        order,
134                        created,
135                        unnamed: true,
136                    },
137                );
138                (buffer_name, true)
139            }
140        };
141
142        let mut evicted = Vec::new();
143        if is_new_unnamed && buffer_limit > 0 {
144            let unnamed_count = self.buffers.values().filter(|entry| entry.unnamed).count();
145            if unnamed_count > buffer_limit {
146                let to_evict = unnamed_count - buffer_limit;
147                let mut unnamed_by_order: Vec<(String, u64)> = self
148                    .buffers
149                    .iter()
150                    .filter(|(_, entry)| entry.unnamed)
151                    .map(|(name, entry)| (name.clone(), entry.order))
152                    .collect();
153                unnamed_by_order.sort_by_key(|(_, order)| *order);
154
155                for (name, _) in unnamed_by_order.into_iter().take(to_evict) {
156                    self.buffers.remove(&name);
157                    evicted.push(name);
158                }
159            }
160        }
161
162        Ok(SetBufferOutcome {
163            buffer_name: Some(buffer_name),
164            evicted,
165        })
166    }
167
168    /// Renames a buffer, replacing any existing destination buffer.
169    pub fn rename(
170        &mut self,
171        old_name: Option<&str>,
172        new_name: &str,
173    ) -> Result<RenameBufferOutcome, RmuxError> {
174        validate_buffer_name(new_name)?;
175
176        let old_name = match old_name {
177            Some(name) => {
178                if !self.buffers.contains_key(name) {
179                    return Err(RmuxError::Server(format!("no buffer {name}")));
180                }
181                name.to_owned()
182            }
183            None => self
184                .top_unnamed()
185                .map(str::to_owned)
186                .ok_or_else(|| RmuxError::Server("no buffer".to_owned()))?,
187        };
188
189        if old_name == new_name {
190            return Ok(RenameBufferOutcome {
191                old_name,
192                new_name: new_name.to_owned(),
193                replaced: false,
194                changed: false,
195            });
196        }
197
198        let replaced = self.buffers.remove(new_name).is_some();
199        let mut entry = self
200            .buffers
201            .remove(&old_name)
202            .expect("rename source existence was prevalidated");
203        entry.unnamed = false;
204        self.buffers.insert(new_name.to_owned(), entry);
205
206        Ok(RenameBufferOutcome {
207            old_name,
208            new_name: new_name.to_owned(),
209            replaced,
210            changed: true,
211        })
212    }
213
214    /// Deletes a buffer by name.
215    ///
216    /// When `name` is `None`, deletes the stack-head buffer.
217    /// Returns the name of the deleted buffer.
218    pub fn delete(&mut self, name: Option<&str>) -> Result<String, RmuxError> {
219        let target = match name {
220            Some(name) => name.to_owned(),
221            None => self
222                .stack_head()
223                .map(str::to_owned)
224                .ok_or_else(|| RmuxError::Server("no buffers".to_owned()))?,
225        };
226
227        if self.buffers.remove(&target).is_none() {
228            return Err(RmuxError::Server(format!("no buffer {target}")));
229        }
230
231        Ok(target)
232    }
233
234    /// Deletes a buffer only when the current entry still matches `order`.
235    ///
236    /// Returns `true` when the matching entry was removed. Returns `false`
237    /// when the buffer is absent or has since been replaced.
238    pub fn delete_if_order_matches(&mut self, name: &str, order: u64) -> bool {
239        if self
240            .buffers
241            .get(name)
242            .is_some_and(|entry| entry.order == order)
243        {
244            self.buffers.remove(name);
245            true
246        } else {
247            false
248        }
249    }
250
251    /// Returns the content of a buffer by name, or the stack-head buffer when
252    /// `name` is `None`.
253    pub fn show(&self, name: Option<&str>) -> Result<(&str, &[u8]), RmuxError> {
254        let (name, content, _) = self.show_with_order(name)?;
255        Ok((name, content))
256    }
257
258    /// Returns the content and current replacement order for a buffer.
259    pub fn show_with_order(&self, name: Option<&str>) -> Result<(&str, &[u8], u64), RmuxError> {
260        let resolved = match name {
261            Some(name) => {
262                if !self.buffers.contains_key(name) {
263                    return Err(RmuxError::Server(format!("no buffer {name}")));
264                }
265                name.to_owned()
266            }
267            None => self
268                .stack_head()
269                .ok_or_else(|| RmuxError::Server("no buffers".to_owned()))?
270                .to_owned(),
271        };
272
273        let (key, entry) = self
274            .buffers
275            .get_key_value(&resolved)
276            .expect("buffer existence was verified above");
277        Ok((key.as_str(), &entry.content, entry.order))
278    }
279
280    /// Returns immutable buffer entries for server-side formatting and sorting.
281    #[must_use]
282    pub fn entries(&self) -> Vec<BufferView<'_>> {
283        self.buffers
284            .iter()
285            .map(|(name, entry)| BufferView {
286                name,
287                content: &entry.content,
288                order: entry.order,
289                created: entry.created,
290            })
291            .collect()
292    }
293
294    /// Returns a formatted listing of all buffers, ordered by most recent first.
295    #[must_use]
296    pub fn list(&self) -> Vec<String> {
297        let mut entries = self.entries();
298        entries.sort_by_key(|entry| std::cmp::Reverse(entry.order));
299        entries
300            .into_iter()
301            .map(|entry| entry.default_line())
302            .collect()
303    }
304
305    fn allocate_unnamed_name(&mut self) -> Result<String, RmuxError> {
306        loop {
307            let id = self.next_unnamed_id;
308            self.next_unnamed_id = self
309                .next_unnamed_id
310                .checked_add(1)
311                .ok_or_else(|| RmuxError::Server("unnamed buffer sequence exhausted".to_owned()))?;
312            let name = format!("buffer{id}");
313            if !self.buffers.contains_key(&name) {
314                return Ok(name);
315            }
316        }
317    }
318}
319
320/// The outcome of a `set` operation on the buffer store.
321#[derive(Debug, Clone, PartialEq, Eq)]
322pub struct SetBufferOutcome {
323    /// The name of the created or replaced buffer when one was stored.
324    buffer_name: Option<String>,
325    /// Names of unnamed buffers evicted by FIFO eviction (oldest first).
326    evicted: Vec<String>,
327}
328
329impl SetBufferOutcome {
330    /// Returns the name of the created or replaced buffer.
331    #[must_use]
332    pub fn buffer_name(&self) -> Option<&str> {
333        self.buffer_name.as_deref()
334    }
335
336    /// Returns the names of evicted unnamed buffers.
337    #[must_use]
338    pub fn evicted(&self) -> &[String] {
339        &self.evicted
340    }
341}
342
343/// Outcome details for a rename operation.
344#[derive(Debug, Clone, PartialEq, Eq)]
345pub struct RenameBufferOutcome {
346    old_name: String,
347    new_name: String,
348    replaced: bool,
349    changed: bool,
350}
351
352impl RenameBufferOutcome {
353    /// Returns the previous buffer name.
354    #[must_use]
355    pub fn old_name(&self) -> &str {
356        &self.old_name
357    }
358
359    /// Returns the resulting buffer name.
360    #[must_use]
361    pub fn new_name(&self) -> &str {
362        &self.new_name
363    }
364
365    /// Returns whether an existing destination buffer was replaced.
366    #[must_use]
367    pub const fn replaced(&self) -> bool {
368        self.replaced
369    }
370
371    /// Returns whether the rename changed the store.
372    #[must_use]
373    pub const fn changed(&self) -> bool {
374        self.changed
375    }
376}
377
378/// One borrow-only buffer view for list and format rendering.
379#[derive(Debug, Clone, Copy, PartialEq, Eq)]
380pub struct BufferView<'a> {
381    name: &'a str,
382    content: &'a [u8],
383    order: u64,
384    created: i64,
385}
386
387impl<'a> BufferView<'a> {
388    /// Returns the tmux-visible buffer name.
389    #[must_use]
390    pub fn name(&self) -> &'a str {
391        self.name
392    }
393
394    /// Returns the raw binary buffer content.
395    #[must_use]
396    pub fn content(&self) -> &'a [u8] {
397        self.content
398    }
399
400    /// Returns the monotonic stack-order value.
401    #[must_use]
402    pub const fn order(&self) -> u64 {
403        self.order
404    }
405
406    /// Returns the tmux-compatible created timestamp.
407    #[must_use]
408    pub const fn created(&self) -> i64 {
409        self.created
410    }
411
412    /// Returns the buffer size in bytes.
413    #[must_use]
414    pub fn size(&self) -> usize {
415        self.content.len()
416    }
417
418    /// Returns the tmux-compatible sample preview.
419    #[must_use]
420    pub fn sample(&self) -> String {
421        buffer_preview(self.content)
422    }
423
424    /// Returns the default `list-buffers` line for this entry.
425    #[must_use]
426    pub fn default_line(&self) -> String {
427        format!(
428            "{}: {} bytes: \"{}\"",
429            self.name(),
430            self.size(),
431            self.sample()
432        )
433    }
434}
435
436/// Validates a buffer name: tmux only rejects empty names.
437fn validate_buffer_name(name: &str) -> Result<(), RmuxError> {
438    if name.is_empty() {
439        return Err(RmuxError::Server(
440            "buffer name must not be empty".to_owned(),
441        ));
442    }
443    Ok(())
444}
445
446/// Returns a short preview of buffer content for `list-buffers` output.
447fn buffer_preview(content: &[u8]) -> String {
448    encode_buffer_sample(content)
449}
450
451#[cfg(test)]
452#[path = "buffers/tests.rs"]
453mod tests;