Skip to main content

ratatui_zonekit/
zone.rs

1//! Zone types — identifiers, hints, requests, and specs.
2//!
3//! A zone is a named rectangular area in the terminal that a plugin
4//! can own and render into. Zones are created by the host application
5//! in response to plugin requests.
6
7use ratatui::layout::Rect;
8
9/// Unique identifier for a zone, assigned by the host.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub struct ZoneId(u32);
12
13impl ZoneId {
14    /// Creates a new zone identifier.
15    #[must_use]
16    pub const fn new(id: u32) -> Self {
17        Self(id)
18    }
19
20    /// Returns the raw numeric identifier.
21    #[must_use]
22    pub const fn raw(self) -> u32 {
23        self.0
24    }
25}
26
27impl std::fmt::Display for ZoneId {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        write!(f, "zone:{}", self.0)
30    }
31}
32
33/// Where a plugin wants its zone to appear.
34///
35/// The host maps these hints to actual layout positions. A hint is
36/// a preference, not a guarantee — the host may ignore or reposition
37/// based on available space and terminal tier.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum ZoneHint {
40    /// Main content area (replaces default content when active).
41    Tab,
42    /// Sidebar column (Standard + Wide tiers).
43    Sidebar,
44    /// Control column (Wide tier only).
45    Control,
46    /// Floating overlay on top of all zones.
47    Overlay,
48    /// Status bar area (single line, bottom).
49    StatusBar,
50}
51
52/// A plugin's request to create or own a zone.
53///
54/// Plugins submit requests during registration. The host evaluates
55/// each request and either grants a [`ZoneId`] or denies it.
56#[derive(Debug, Clone)]
57pub struct ZoneRequest {
58    /// Namespaced identifier: `"{plugin_id}.{local_name}"`.
59    pub name: String,
60    /// Where the plugin wants the zone.
61    pub hint: ZoneHint,
62    /// Display label (for tabs, panel headers).
63    pub label: String,
64    /// Preferred height in lines (0 = fill available). Ignored for tabs.
65    pub preferred_height: u16,
66    /// Preferred width in columns (0 = fill available). Ignored for tabs.
67    pub preferred_width: u16,
68    /// Minimum terminal width for this zone to appear (0 = always).
69    pub min_terminal_width: u16,
70    /// Display order within the zone hint (lower = first).
71    pub order: u8,
72}
73
74impl ZoneRequest {
75    /// Creates a tab zone request.
76    #[must_use]
77    pub fn tab(name: impl Into<String>, label: impl Into<String>) -> Self {
78        Self {
79            name: name.into(),
80            hint: ZoneHint::Tab,
81            label: label.into(),
82            preferred_height: 0,
83            preferred_width: 0,
84            min_terminal_width: 0,
85            order: 128,
86        }
87    }
88
89    /// Creates a sidebar panel zone request.
90    #[must_use]
91    pub fn sidebar(name: impl Into<String>, label: impl Into<String>) -> Self {
92        Self {
93            name: name.into(),
94            hint: ZoneHint::Sidebar,
95            label: label.into(),
96            preferred_height: 0,
97            preferred_width: 0,
98            min_terminal_width: 120,
99            order: 128,
100        }
101    }
102
103    /// Creates a floating overlay zone request.
104    #[must_use]
105    pub fn overlay(
106        name: impl Into<String>,
107        label: impl Into<String>,
108        width: u16,
109        height: u16,
110    ) -> Self {
111        Self {
112            name: name.into(),
113            hint: ZoneHint::Overlay,
114            label: label.into(),
115            preferred_height: height,
116            preferred_width: width,
117            min_terminal_width: 0,
118            order: 0,
119        }
120    }
121
122    /// Sets the display order.
123    #[must_use]
124    pub fn with_order(mut self, order: u8) -> Self {
125        self.order = order;
126        self
127    }
128
129    /// Sets the minimum terminal width.
130    #[must_use]
131    pub fn with_min_width(mut self, width: u16) -> Self {
132        self.min_terminal_width = width;
133        self
134    }
135}
136
137/// A resolved zone — the host's response to a [`ZoneRequest`].
138///
139/// Contains the allocated area and metadata. Updated every frame
140/// since terminal resize can change the allocated rect.
141#[derive(Debug, Clone)]
142pub struct ZoneSpec {
143    /// Unique zone identifier.
144    pub id: ZoneId,
145    /// Original request name.
146    pub name: String,
147    /// Display label.
148    pub label: String,
149    /// Zone hint (where it was placed).
150    pub hint: ZoneHint,
151    /// Currently allocated area (updated per frame).
152    pub area: Rect,
153    /// Whether this zone is currently visible.
154    pub visible: bool,
155    /// Display order used for sorting.
156    pub order: u8,
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn zone_id_display() {
165        assert_eq!(ZoneId::new(42).to_string(), "zone:42");
166    }
167
168    #[test]
169    fn zone_id_equality() {
170        assert_eq!(ZoneId::new(1), ZoneId::new(1));
171        assert_ne!(ZoneId::new(1), ZoneId::new(2));
172    }
173
174    #[test]
175    fn zone_request_tab() {
176        let req = ZoneRequest::tab("bmad.sprint", "Sprint");
177        assert_eq!(req.hint, ZoneHint::Tab);
178        assert_eq!(req.name, "bmad.sprint");
179        assert_eq!(req.label, "Sprint");
180    }
181
182    #[test]
183    fn zone_request_sidebar() {
184        let req = ZoneRequest::sidebar("github.pr", "Pull Requests");
185        assert_eq!(req.hint, ZoneHint::Sidebar);
186        assert_eq!(req.min_terminal_width, 120);
187    }
188
189    #[test]
190    fn zone_request_overlay() {
191        let req = ZoneRequest::overlay("finder.search", "Search", 60, 20);
192        assert_eq!(req.hint, ZoneHint::Overlay);
193        assert_eq!(req.preferred_width, 60);
194        assert_eq!(req.preferred_height, 20);
195    }
196
197    #[test]
198    fn zone_request_builder() {
199        let req = ZoneRequest::tab("test.tab", "Test")
200            .with_order(10)
201            .with_min_width(140);
202        assert_eq!(req.order, 10);
203        assert_eq!(req.min_terminal_width, 140);
204    }
205}