use std::collections::HashMap;
use std::sync::Arc;
use ratatui::layout::Rect;
use crate::plugin::ZonePlugin;
use crate::zone::{ZoneHint, ZoneId, ZoneSpec};
#[derive(Debug)]
pub enum RegistrationResult {
Granted(ZoneId),
Denied(String),
}
pub struct ZoneRegistry {
next_id: u32,
zones: Vec<ZoneSpec>,
owners: HashMap<ZoneId, Arc<dyn ZonePlugin>>,
}
impl ZoneRegistry {
#[must_use]
pub fn new() -> Self {
Self {
next_id: 1,
zones: Vec::new(),
owners: HashMap::new(),
}
}
#[allow(clippy::needless_pass_by_value)] pub fn register(&mut self, plugin: Arc<dyn ZonePlugin>) -> Vec<RegistrationResult> {
let requests = plugin.zones();
let mut results = Vec::with_capacity(requests.len());
for request in requests {
if self.zones.iter().any(|z| z.name == request.name) {
results.push(RegistrationResult::Denied(format!(
"zone '{}' already registered",
request.name
)));
continue;
}
let id = ZoneId::new(self.next_id);
self.next_id += 1;
self.zones.push(ZoneSpec {
id,
name: request.name.clone(),
label: request.label,
hint: request.hint,
area: Rect::default(),
visible: true,
order: request.order,
});
self.owners.insert(id, Arc::clone(&plugin));
plugin.on_register(id);
results.push(RegistrationResult::Granted(id));
}
results
}
#[must_use]
pub fn zones_by_hint(&self, hint: ZoneHint) -> Vec<&ZoneSpec> {
let mut zones: Vec<&ZoneSpec> = self
.zones
.iter()
.filter(|z| z.hint == hint && z.visible)
.collect();
zones.sort_by_key(|z| z.order);
zones
}
#[must_use]
pub fn tabs(&self) -> Vec<&ZoneSpec> {
self.zones_by_hint(ZoneHint::Tab)
}
#[must_use]
pub fn owner(&self, zone_id: ZoneId) -> Option<&Arc<dyn ZonePlugin>> {
self.owners.get(&zone_id)
}
#[must_use]
pub fn zone(&self, zone_id: ZoneId) -> Option<&ZoneSpec> {
self.zones.iter().find(|z| z.id == zone_id)
}
#[must_use]
pub fn zone_by_name(&self, name: &str) -> Option<&ZoneSpec> {
self.zones.iter().find(|z| z.name == name)
}
pub fn update_area(&mut self, zone_id: ZoneId, area: Rect) {
if let Some(zone) = self.zones.iter_mut().find(|z| z.id == zone_id) {
zone.area = area;
}
}
pub fn set_visible(&mut self, zone_id: ZoneId, visible: bool) {
if let Some(zone) = self.zones.iter_mut().find(|z| z.id == zone_id) {
zone.visible = visible;
}
}
#[must_use]
pub fn len(&self) -> usize {
self.zones.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.zones.is_empty()
}
#[must_use]
pub fn all_zones(&self) -> &[ZoneSpec] {
&self.zones
}
}
impl Default for ZoneRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[allow(clippy::unnecessary_literal_bound)]
mod tests {
use ratatui::buffer::Buffer;
use super::*;
use crate::plugin::RenderContext;
use crate::zone::ZoneRequest;
struct FakePlugin {
id: &'static str,
requests: Vec<ZoneRequest>,
}
impl ZonePlugin for FakePlugin {
fn id(&self) -> &str {
self.id
}
fn zones(&self) -> Vec<ZoneRequest> {
self.requests.clone()
}
fn render(&self, _: ZoneId, _: &RenderContext, _: Rect, _: &mut Buffer) -> bool {
true
}
}
fn plugin_with_tab(id: &'static str, name: &str, label: &str) -> Arc<dyn ZonePlugin> {
Arc::new(FakePlugin {
id,
requests: vec![ZoneRequest::tab(name, label)],
})
}
fn plugin_with_sidebar(id: &'static str, name: &str, label: &str) -> Arc<dyn ZonePlugin> {
Arc::new(FakePlugin {
id,
requests: vec![ZoneRequest::sidebar(name, label)],
})
}
#[test]
fn empty_registry() {
let reg = ZoneRegistry::new();
assert!(reg.is_empty());
assert_eq!(reg.len(), 0);
assert!(reg.tabs().is_empty());
}
#[test]
fn register_plugin_grants_zone() {
let mut reg = ZoneRegistry::new();
let results = reg.register(plugin_with_tab("bmad", "bmad.sprint", "Sprint"));
assert_eq!(results.len(), 1);
assert!(matches!(results[0], RegistrationResult::Granted(_)));
assert_eq!(reg.len(), 1);
}
#[test]
fn duplicate_name_is_denied() {
let mut reg = ZoneRegistry::new();
reg.register(plugin_with_tab("a", "shared.name", "Tab A"));
let results = reg.register(plugin_with_tab("b", "shared.name", "Tab B"));
assert!(matches!(results[0], RegistrationResult::Denied(_)));
assert_eq!(reg.len(), 1);
}
#[test]
fn zones_by_hint_filters_correctly() {
let mut reg = ZoneRegistry::new();
reg.register(plugin_with_tab("a", "a.tab", "Tab A"));
reg.register(plugin_with_sidebar("b", "b.side", "Side B"));
assert_eq!(reg.zones_by_hint(ZoneHint::Tab).len(), 1);
assert_eq!(reg.zones_by_hint(ZoneHint::Sidebar).len(), 1);
assert_eq!(reg.zones_by_hint(ZoneHint::Overlay).len(), 0);
}
#[test]
fn tabs_returns_tab_zones_sorted() {
let mut reg = ZoneRegistry::new();
reg.register(Arc::new(FakePlugin {
id: "b",
requests: vec![ZoneRequest::tab("b.tab", "B").with_order(20)],
}));
reg.register(Arc::new(FakePlugin {
id: "a",
requests: vec![ZoneRequest::tab("a.tab", "A").with_order(10)],
}));
let tabs = reg.tabs();
assert_eq!(tabs[0].label, "A");
assert_eq!(tabs[1].label, "B");
}
#[test]
fn owner_returns_plugin() {
let mut reg = ZoneRegistry::new();
let plugin = plugin_with_tab("test", "test.tab", "Test");
let results = reg.register(plugin);
if let RegistrationResult::Granted(id) = &results[0] {
let owner = reg.owner(*id).unwrap();
assert_eq!(owner.id(), "test");
}
}
#[test]
fn zone_by_name() {
let mut reg = ZoneRegistry::new();
reg.register(plugin_with_tab("x", "x.tab", "X"));
assert!(reg.zone_by_name("x.tab").is_some());
assert!(reg.zone_by_name("nonexistent").is_none());
}
#[test]
fn update_area() {
let mut reg = ZoneRegistry::new();
let results = reg.register(plugin_with_tab("x", "x.tab", "X"));
if let RegistrationResult::Granted(id) = &results[0] {
let new_area = Rect::new(10, 20, 80, 40);
reg.update_area(*id, new_area);
assert_eq!(reg.zone(*id).unwrap().area, new_area);
}
}
#[test]
fn set_visible_hides_zone() {
let mut reg = ZoneRegistry::new();
let results = reg.register(plugin_with_tab("x", "x.tab", "X"));
if let RegistrationResult::Granted(id) = &results[0] {
reg.set_visible(*id, false);
assert!(reg.tabs().is_empty(), "hidden tab should not appear");
}
}
#[test]
fn multiple_plugins_get_unique_ids() {
let mut reg = ZoneRegistry::new();
let r1 = reg.register(plugin_with_tab("a", "a.tab", "A"));
let r2 = reg.register(plugin_with_tab("b", "b.tab", "B"));
if let (RegistrationResult::Granted(id1), RegistrationResult::Granted(id2)) =
(&r1[0], &r2[0])
{
assert_ne!(id1, id2);
}
}
}