use std::collections::VecDeque;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueuedMessage {
pub id: usize,
pub text: String,
}
#[derive(Default)]
pub struct MessageQueue {
entries: VecDeque<QueuedMessage>,
next_id: usize,
}
impl MessageQueue {
pub fn push(&mut self, text: impl Into<String>) -> usize {
let id = self.next_id;
self.next_id += 1;
self.entries.push_back(QueuedMessage { id, text: text.into() });
id
}
pub fn pop(&mut self) -> Option<QueuedMessage> {
self.entries.pop_front()
}
pub fn list(&self) -> Vec<&QueuedMessage> {
self.entries.iter().collect()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn remove(&mut self, ids: &[usize]) -> usize {
let before = self.entries.len();
self.entries.retain(|entry| !ids.contains(&entry.id));
before - self.entries.len()
}
pub fn edit(&mut self, id: usize, text: impl Into<String>) -> bool {
match self.entries.iter_mut().find(|entry| entry.id == id) {
Some(entry) => {
entry.text = text.into();
true
}
None => false,
}
}
pub fn clear(&mut self) -> usize {
let n = self.entries.len();
self.entries.clear();
n
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QueueOp {
List,
Remove(Vec<usize>),
Edit { id: usize, text: String },
Clear,
Add(String),
}
const USAGE: &str = "usage: /queue [list] | /queue add text | /queue remove N... | /queue edit N text | /queue clear";
pub fn parse(args: &str) -> Result<QueueOp, String> {
let args = args.trim();
if args.is_empty() || args == "list" || args == "ls" {
return Ok(QueueOp::List);
}
let (sub, rest) = args.split_once(char::is_whitespace).unwrap_or((args, ""));
let rest = rest.trim();
match sub {
"add" if rest.is_empty() => Err(USAGE.to_string()),
"add" => Ok(QueueOp::Add(rest.to_string())),
"remove" | "rm" | "delete" => {
let mut ids = Vec::new();
for word in rest.split_whitespace() {
let id = word
.parse::<usize>()
.map_err(|_| format!("{word:?} is not a queue id — {USAGE}"))?;
ids.push(id);
}
if ids.is_empty() {
return Err(USAGE.to_string());
}
Ok(QueueOp::Remove(ids))
}
"edit" => {
let (id, text) = rest
.split_once(char::is_whitespace)
.ok_or_else(|| USAGE.to_string())?;
let id = id
.trim()
.parse::<usize>()
.map_err(|_| format!("{id:?} is not a queue id — {USAGE}"))?;
let text = text.trim();
if text.is_empty() {
return Err(USAGE.to_string());
}
Ok(QueueOp::Edit { id, text: text.to_string() })
}
"clear" => {
if !rest.is_empty() {
return Err(USAGE.to_string());
}
Ok(QueueOp::Clear)
}
_ => Err(USAGE.to_string()),
}
}
pub fn apply(queue: &mut MessageQueue, op: &QueueOp) -> String {
match op {
QueueOp::List => describe(queue),
QueueOp::Remove(ids) => {
let removed = queue.remove(ids);
if removed == 0 {
format!("No queued message with {} ({})", ids_text(ids), queue_summary(queue))
} else {
format!("Removed {removed} queued message{} ({})", plural(removed), queue_summary(queue))
}
}
QueueOp::Edit { id, text } => {
if queue.edit(*id, text.clone()) {
format!("Queue #{id} updated: {}", preview(text, 60))
} else {
format!("No queued message with id {id} ({})", queue_summary(queue))
}
}
QueueOp::Add(text) => {
let id = queue.push(text.clone());
format!("↧ queued #{id} ({}) — /queue remove {id} to drop", queue_summary(queue))
}
QueueOp::Clear => {
let n = queue.clear();
format!("Cleared {n} queued message{}", plural(n))
}
}
}
pub fn describe(queue: &MessageQueue) -> String {
if queue.is_empty() {
return "Queue is empty. Ctrl-Enter (or /queue add) while a turn runs queues a message; plain Enter steers.".to_string();
}
let mut out = format!("Queued ({}):", queue.len());
for entry in queue.list() {
out.push_str(&format!("\n {:>3} {}", entry.id, preview(&entry.text, 80)));
}
out
}
fn queue_summary(queue: &MessageQueue) -> String {
match queue.len() {
0 => "queue is empty".to_string(),
n => format!("{n} queued"),
}
}
fn plural(n: usize) -> &'static str {
if n == 1 { "" } else { "s" }
}
fn ids_text(ids: &[usize]) -> String {
match ids {
[id] => format!("id {id}"),
ids => format!("ids {}", ids.iter().map(usize::to_string).collect::<Vec<_>>().join(", ")),
}
}
fn preview(text: &str, max: usize) -> String {
let one_line: String = text.split_whitespace().collect::<Vec<_>>().join(" ");
let count = one_line.chars().count();
if count <= max {
one_line
} else {
format!("{}…", one_line.chars().take(max.saturating_sub(1)).collect::<String>())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn push_assigns_monotonic_ids_that_survive_removal_and_clear() {
let mut queue = MessageQueue::default();
let a = queue.push("first");
let b = queue.push("second");
let c = queue.push("third");
assert_eq!((a, b, c), (0, 1, 2));
assert_eq!(queue.remove(&[b]), 1);
let remaining: Vec<usize> = queue.list().iter().map(|m| m.id).collect();
assert_eq!(remaining, [0, 2], "ids are not renumbered");
assert_eq!(queue.clear(), 2);
assert!(queue.is_empty());
let d = queue.push("fourth");
assert_eq!(d, 3, "ids keep climbing after a clear");
}
#[test]
fn pop_is_fifo_and_edit_rewrites_in_place() {
let mut queue = MessageQueue::default();
queue.push("one");
queue.push("two");
assert!(queue.edit(1, "TWO"));
assert!(!queue.edit(9, "nope"), "unknown id rejected");
assert_eq!(queue.pop().unwrap().text, "one");
assert_eq!(queue.pop().unwrap().text, "TWO");
assert!(queue.pop().is_none());
}
#[test]
fn parse_covers_the_subcommands() {
assert_eq!(parse(""), Ok(QueueOp::List));
assert_eq!(parse(" "), Ok(QueueOp::List));
assert_eq!(parse("list"), Ok(QueueOp::List));
assert_eq!(parse("ls"), Ok(QueueOp::List));
assert_eq!(parse("remove 2"), Ok(QueueOp::Remove(vec![2])));
assert_eq!(parse("rm 2 4"), Ok(QueueOp::Remove(vec![2, 4])));
assert_eq!(parse("delete 3"), Ok(QueueOp::Remove(vec![3])));
assert_eq!(parse("edit 2 try this instead"), Ok(QueueOp::Edit { id: 2, text: "try this instead".into() }));
assert_eq!(parse("clear"), Ok(QueueOp::Clear));
assert!(parse("clear keep-this").is_err(), "clear rejects trailing arguments");
assert!(parse("remove").is_err(), "remove needs an id");
assert!(parse("remove x").is_err(), "ids are numbers");
assert!(parse("edit 2").is_err(), "edit needs replacement text");
assert!(parse("edit 2 ").is_err(), "blank replacement rejected");
assert!(parse("edit x hi").is_err());
assert!(parse("frob").is_err(), "unknown subcommand shows usage");
}
#[test]
fn apply_reports_what_happened() {
let mut queue = MessageQueue::default();
queue.push("alpha");
queue.push("beta");
queue.push("gamma");
let listed = apply(&mut queue, &QueueOp::List);
assert!(listed.contains("Queued (3):"), "{listed}");
assert!(listed.contains(" 0 alpha") && listed.contains(" 2 gamma"), "{listed}");
let removed = apply(&mut queue, &QueueOp::Remove(vec![1, 9]));
assert!(removed.contains("Removed 1 queued message"), "{removed}");
assert!(removed.contains("(2 queued)"), "{removed}");
let edited = apply(&mut queue, &QueueOp::Edit { id: 2, text: "GAMMA".into() });
assert!(edited.contains("#2"), "{edited}");
assert_eq!(queue.list()[1].text, "GAMMA");
let missing = apply(&mut queue, &QueueOp::Remove(vec![7]));
assert!(missing.contains("No queued message with id 7"), "{missing}");
assert!(apply(&mut queue, &QueueOp::Clear).contains("Cleared 2"));
assert_eq!(apply(&mut queue, &QueueOp::List), "Queue is empty. Ctrl-Enter (or /queue add) while a turn runs queues a message; plain Enter steers.");
}
#[test]
fn preview_flattens_and_truncates() {
assert_eq!(preview("hello\n world", 80), "hello world");
let long = "x".repeat(100);
let shown = preview(&long, 10);
assert_eq!(shown.chars().count(), 10);
assert!(shown.ends_with('…'));
}
#[test]
fn add_queues_a_message() {
assert_eq!(parse("add fix the build").unwrap(), QueueOp::Add("fix the build".into()));
assert!(parse("add").is_err());
assert!(parse("add ").is_err());
let mut queue = MessageQueue::default();
let note = apply(&mut queue, &QueueOp::Add("later".into()));
assert!(note.starts_with("↧ queued #") && note.contains("/queue remove"), "{note}");
assert_eq!(queue.len(), 1);
}
}