Skip to main content

pacsea/state/
main_vertical_pane.rs

1//! Vertical main-stack roles: results list, middle search row, and package info.
2
3/// What: Identifies one of the three vertical regions in the main TUI stack.
4///
5/// Inputs: None (enum definition).
6///
7/// Output: None (enum definition).
8///
9/// Details:
10/// - Distinct from horizontal [`crate::state::Focus`]; this only describes top-to-bottom placement.
11/// - The active permutation is stored in [`crate::theme::Settings::main_pane_order`] and copied to
12///   [`crate::state::AppState::main_pane_order`] at startup and on config reload.
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
14pub enum MainVerticalPane {
15    /// Package search results list (title row + list).
16    Results,
17    /// Middle row: recent queries, search input, install list.
18    Middle,
19    /// Package details / news reader body.
20    PackageInfo,
21}
22
23/// What: Default top-to-bottom order (results, then middle search row, then package info).
24///
25/// Inputs: None (constant).
26///
27/// Output: Constant array value.
28///
29/// Details:
30/// - Matches the historical layout before `main_pane_order` existed.
31pub const DEFAULT_MAIN_PANE_ORDER: [MainVerticalPane; 3] = [
32    MainVerticalPane::Results,
33    MainVerticalPane::Middle,
34    MainVerticalPane::PackageInfo,
35];
36
37impl MainVerticalPane {
38    /// What: Serialize this role to a stable settings token (lowercase).
39    ///
40    /// Inputs:
41    /// - `self`: Pane role.
42    ///
43    /// Output:
44    /// - Canonical token string.
45    ///
46    /// Details:
47    /// - Used when writing defaults into `settings.conf`.
48    #[must_use]
49    pub const fn as_config_token(self) -> &'static str {
50        match self {
51            Self::Results => "results",
52            Self::Middle => "search",
53            Self::PackageInfo => "package_info",
54        }
55    }
56
57    /// What: Parse a single role token from config (case-insensitive).
58    ///
59    /// Inputs:
60    /// - `token`: One comma-separated fragment from `main_pane_order`.
61    ///
62    /// Output:
63    /// - `Some(role)` when recognized, else `None`.
64    ///
65    /// Details:
66    /// - Accepts aliases: `middle` for search row, `details` for package info.
67    #[must_use]
68    pub fn from_config_token(token: &str) -> Option<Self> {
69        let t = token.trim().to_ascii_lowercase().replace(['-', ' '], "_");
70        match t.as_str() {
71            "results" => Some(Self::Results),
72            "search" | "middle" => Some(Self::Middle),
73            "package_info" | "details" | "packageinfo" => Some(Self::PackageInfo),
74            _ => None,
75        }
76    }
77}
78
79/// What: Parse `main_pane_order` value into a length-3 permutation of distinct roles.
80///
81/// Inputs:
82/// - `value`: Comma-separated tokens (whitespace allowed).
83///
84/// Output:
85/// - `Some(order)` when exactly three distinct known roles are present; otherwise `None`.
86///
87/// Details:
88/// - Empty or duplicate roles yield `None`.
89#[must_use]
90pub fn parse_main_pane_order(value: &str) -> Option<[MainVerticalPane; 3]> {
91    let parts: Vec<&str> = value
92        .split(',')
93        .map(str::trim)
94        .filter(|s| !s.is_empty())
95        .collect();
96    if parts.len() != 3 {
97        return None;
98    }
99    let mut seen = [false; 3];
100    let mut out = [MainVerticalPane::Results; 3];
101    for (i, part) in parts.iter().enumerate() {
102        let role = MainVerticalPane::from_config_token(part)?;
103        let idx = match role {
104            MainVerticalPane::Results => 0usize,
105            MainVerticalPane::Middle => 1usize,
106            MainVerticalPane::PackageInfo => 2usize,
107        };
108        if seen[idx] {
109            return None;
110        }
111        seen[idx] = true;
112        out[i] = role;
113    }
114    Some(out)
115}
116
117/// What: Format a pane order for `settings.conf` (canonical tokens).
118///
119/// Inputs:
120/// - `order`: Three distinct roles.
121///
122/// Output:
123/// - Comma+space separated string.
124///
125/// Details:
126/// - Intended for `ensure_settings_keys_present` and tests.
127#[must_use]
128pub fn format_main_pane_order(order: &[MainVerticalPane; 3]) -> String {
129    format!(
130        "{}, {}, {}",
131        order[0].as_config_token(),
132        order[1].as_config_token(),
133        order[2].as_config_token()
134    )
135}
136
137/// What: Min/max row heights for vertical layout allocation (semantic per pane).
138///
139/// Inputs: None (struct definition).
140///
141/// Output: None (struct definition).
142///
143/// Details:
144/// - Values are applied after parsing and normalization from `settings.conf`.
145/// - Package info has no user `max`; the allocator assigns remaining rows.
146#[derive(Clone, Copy, Debug, PartialEq, Eq)]
147pub struct VerticalLayoutLimits {
148    /// Minimum height (terminal rows) for the results list region.
149    pub min_results: u16,
150    /// Maximum height for the results list region.
151    pub max_results: u16,
152    /// Minimum height for the middle (search) row.
153    pub min_middle: u16,
154    /// Maximum height for the middle row.
155    pub max_middle: u16,
156    /// Minimum height for package info when that band is visible.
157    pub min_package_info: u16,
158}
159
160impl Default for VerticalLayoutLimits {
161    /// What: Match historical hardcoded [`crate::ui`] constraints.
162    fn default() -> Self {
163        Self {
164            min_results: 3,
165            max_results: 17,
166            min_middle: 3,
167            max_middle: 5,
168            min_package_info: 3,
169        }
170    }
171}
172
173impl VerticalLayoutLimits {
174    /// What: Build limits from normalized numeric settings (avoids `state` ↔ `theme` cycles).
175    ///
176    /// Inputs:
177    /// - `min_results`, `max_results`, `min_middle`, `max_middle`, `min_package_info`: Parsed settings.
178    ///
179    /// Output:
180    /// - Populated limits struct.
181    #[must_use]
182    pub const fn from_u16s(
183        min_results: u16,
184        max_results: u16,
185        min_middle: u16,
186        max_middle: u16,
187        min_package_info: u16,
188    ) -> Self {
189        Self {
190            min_results,
191            max_results,
192            min_middle,
193            max_middle,
194            min_package_info,
195        }
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    #[test]
204    fn parse_order_default_tokens() {
205        let o = parse_main_pane_order("results, search, package_info").expect("parse");
206        assert_eq!(o, DEFAULT_MAIN_PANE_ORDER);
207    }
208
209    #[test]
210    fn parse_order_aliases_and_permutation() {
211        let o = parse_main_pane_order("package_info,middle,results").expect("parse");
212        assert_eq!(
213            o,
214            [
215                MainVerticalPane::PackageInfo,
216                MainVerticalPane::Middle,
217                MainVerticalPane::Results
218            ]
219        );
220    }
221
222    #[test]
223    fn parse_order_rejects_duplicates_and_bad_length() {
224        assert!(parse_main_pane_order("results,results,middle").is_none());
225        assert!(parse_main_pane_order("results,middle").is_none());
226        assert!(parse_main_pane_order("a,b,c").is_none());
227    }
228}