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}