pacsea/state/types.rs
1//! Core value types used by Pacsea state.
2
3use zeroize::Zeroize;
4
5/// What: Zeroizing wrapper for sensitive in-memory string data such as passwords.
6///
7/// Inputs:
8/// - Constructed from owned string data via [`From<String>`], [`From<&str>`], or [`SecureString::new`].
9///
10/// Output:
11/// - Provides read-only string access while ensuring secret bytes are wiped on drop.
12///
13/// Details:
14/// - The inner buffer is zeroized before deallocation to reduce residual secret exposure.
15/// - `Debug` output is intentionally redacted and never reveals the secret value.
16#[derive(Clone, Default, PartialEq, Eq)]
17pub struct SecureString(String);
18
19impl SecureString {
20 /// What: Create a new zeroizing string wrapper from owned string data.
21 ///
22 /// Inputs:
23 /// - `value`: Secret string to store.
24 ///
25 /// Output:
26 /// - New [`SecureString`] containing `value`.
27 ///
28 /// Details:
29 /// - Ownership is moved into the wrapper so drop-time zeroization covers this allocation.
30 #[must_use]
31 pub const fn new(value: String) -> Self {
32 Self(value)
33 }
34
35 /// What: Borrow the wrapped secret as an immutable string slice.
36 ///
37 /// Inputs:
38 /// - `self`: Borrowed secure string instance.
39 ///
40 /// Output:
41 /// - `&str` view of the wrapped value.
42 ///
43 /// Details:
44 /// - Intended for short-lived read usage (validation and command construction).
45 #[must_use]
46 pub fn as_str(&self) -> &str {
47 &self.0
48 }
49
50 /// What: Return the current number of bytes in the wrapped secret.
51 ///
52 /// Inputs:
53 /// - `self`: Borrowed secure string instance.
54 ///
55 /// Output:
56 /// - Byte length of the underlying UTF-8 buffer.
57 ///
58 /// Details:
59 /// - Mirrors `String::len` and is used by cursor movement logic in password input handling.
60 #[must_use]
61 pub const fn len(&self) -> usize {
62 self.0.len()
63 }
64
65 /// What: Check whether the wrapped secret is empty.
66 ///
67 /// Inputs:
68 /// - `self`: Borrowed secure string instance.
69 ///
70 /// Output:
71 /// - `true` when no bytes are present, otherwise `false`.
72 ///
73 /// Details:
74 /// - Mirrors `String::is_empty`.
75 #[must_use]
76 pub const fn is_empty(&self) -> bool {
77 self.0.is_empty()
78 }
79
80 /// What: Insert a character into the wrapped secret at a byte index.
81 ///
82 /// Inputs:
83 /// - `idx`: Byte position where `ch` is inserted.
84 /// - `ch`: Character to insert.
85 ///
86 /// Output:
87 /// - Mutates the wrapped secret in place.
88 ///
89 /// Details:
90 /// - Panics if `idx` is not on a valid UTF-8 boundary, matching `String::insert`.
91 pub fn insert(&mut self, idx: usize, ch: char) {
92 self.0.insert(idx, ch);
93 }
94
95 /// What: Remove and return a character from the wrapped secret at a byte index.
96 ///
97 /// Inputs:
98 /// - `idx`: Byte position of the character to remove.
99 ///
100 /// Output:
101 /// - Removed `char` value.
102 ///
103 /// Details:
104 /// - Panics if `idx` is not on a valid UTF-8 boundary, matching `String::remove`.
105 pub fn remove(&mut self, idx: usize) -> char {
106 self.0.remove(idx)
107 }
108
109 /// What: Append a character to the wrapped secret.
110 ///
111 /// Inputs:
112 /// - `ch`: Character to append.
113 ///
114 /// Output:
115 /// - Mutates the wrapped secret in place.
116 ///
117 /// Details:
118 /// - Mirrors `String::push` while keeping ownership in the secure wrapper.
119 pub fn push(&mut self, ch: char) {
120 self.0.push(ch);
121 }
122
123 /// What: Clear all bytes from the wrapped secret.
124 ///
125 /// Inputs:
126 /// - `self`: Mutable secure string instance.
127 ///
128 /// Output:
129 /// - Empties the wrapped string.
130 ///
131 /// Details:
132 /// - Mirrors `String::clear` for controlled reset paths.
133 pub fn clear(&mut self) {
134 self.0.clear();
135 }
136}
137
138impl std::ops::Deref for SecureString {
139 type Target = str;
140
141 fn deref(&self) -> &Self::Target {
142 self.as_str()
143 }
144}
145
146impl std::fmt::Debug for SecureString {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 f.write_str("SecureString([REDACTED])")
149 }
150}
151
152impl From<String> for SecureString {
153 fn from(value: String) -> Self {
154 Self::new(value)
155 }
156}
157
158impl From<&str> for SecureString {
159 fn from(value: &str) -> Self {
160 Self::new(value.to_string())
161 }
162}
163
164impl Drop for SecureString {
165 fn drop(&mut self) {
166 self.0.zeroize();
167 }
168}
169
170/// Minimal news entry for Arch news modal.
171#[derive(Clone, Debug)]
172pub struct NewsItem {
173 /// Publication date (short, e.g., 2025-10-11)
174 pub date: String,
175 /// Title text
176 pub title: String,
177 /// Link URL
178 pub url: String,
179}
180
181/// What: High-level application mode.
182///
183/// Inputs: None (enum variants)
184///
185/// Output: Represents whether the UI is in package management or news view.
186///
187/// Details:
188/// - `Package` preserves the existing package management experience.
189/// - `News` switches panes to the news feed experience.
190#[derive(Clone, Copy, Debug, PartialEq, Eq)]
191pub enum AppMode {
192 /// Package management/search mode (existing UI).
193 Package,
194 /// News feed mode (new UI).
195 News,
196}
197
198/// What: News/advisory source type.
199///
200/// Inputs: None (enum variants)
201///
202/// Output: Identifies where a news feed item originates.
203///
204/// Details:
205/// - Distinguishes Arch news RSS posts from security advisories.
206#[derive(
207 Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
208)]
209pub enum NewsFeedSource {
210 /// Official Arch Linux news RSS item.
211 ArchNews,
212 /// security.archlinux.org advisory.
213 SecurityAdvisory,
214 /// Installed official package received a version update.
215 InstalledPackageUpdate,
216 /// Installed AUR package received a version update.
217 AurPackageUpdate,
218 /// New AUR comment on an installed package.
219 AurComment,
220}
221
222/// What: Severity levels for security advisories.
223///
224/// Inputs: None (enum variants)
225///
226/// Output: Normalized advisory severity.
227///
228/// Details:
229/// - Ordered from lowest to highest severity for sorting.
230#[derive(
231 Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
232)]
233pub enum AdvisorySeverity {
234 /// Unknown or not provided.
235 Unknown,
236 /// Low severity.
237 Low,
238 /// Medium severity.
239 Medium,
240 /// High severity.
241 High,
242 /// Critical severity.
243 Critical,
244}
245
246/// What: Map advisory severity to a numeric rank for sorting (higher is worse).
247///
248/// Inputs:
249/// - `severity`: Optional advisory severity value.
250///
251/// Output:
252/// - Numeric rank where larger numbers indicate higher severity (Critical highest).
253///
254/// Details:
255/// - Returns `0` when severity is missing to ensure advisories without severity fall last.
256/// - Keeps ordering stable across both news feed sorting and advisory-specific listings.
257#[must_use]
258pub const fn severity_rank(severity: Option<AdvisorySeverity>) -> u8 {
259 match severity {
260 Some(AdvisorySeverity::Critical) => 5,
261 Some(AdvisorySeverity::High) => 4,
262 Some(AdvisorySeverity::Medium) => 3,
263 Some(AdvisorySeverity::Low) => 2,
264 Some(AdvisorySeverity::Unknown) => 1,
265 None => 0,
266 }
267}
268
269/// What: Sort options for news feed results.
270///
271/// Inputs: None (enum variants)
272///
273/// Output: Selected sort mode for news items.
274///
275/// Details:
276/// - `DateDesc` is newest-first default.
277#[derive(Clone, Copy, Debug, PartialEq, Eq)]
278pub enum NewsSortMode {
279 /// Newest first by date.
280 DateDesc,
281 /// Oldest first by date.
282 DateAsc,
283 /// Alphabetical by title.
284 Title,
285 /// Group by source then title.
286 SourceThenTitle,
287 /// Severity first (Critical..Unknown), then date (newest first).
288 SeverityThenDate,
289 /// Unread items first, then date (newest first).
290 UnreadThenDate,
291}
292
293/// What: Read filter applied to news feed items.
294///
295/// Inputs: None (enum variants)
296///
297/// Output:
298/// - Indicates whether to show all, only read, or only unread items.
299///
300/// Details:
301/// - Used by the News Feed list and toolbar filter chip.
302#[derive(Clone, Copy, Debug, PartialEq, Eq)]
303pub enum NewsReadFilter {
304 /// Show all items regardless of read status.
305 All,
306 /// Show only items marked as read.
307 Read,
308 /// Show only items not marked as read.
309 Unread,
310}
311
312/// What: Unified news/advisory feed item for the news view.
313///
314/// Inputs:
315/// - Fields describing the item (title, summary, url, source, severity, packages, date)
316///
317/// Output:
318/// - Data ready for list and details rendering in news mode.
319///
320/// Details:
321/// - `id` is a stable identifier (URL for news, advisory ID for security).
322/// - `packages` holds affected package names for advisories.
323#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
324pub struct NewsFeedItem {
325 /// Stable identifier (URL or advisory ID).
326 pub id: String,
327 /// Publication or update date (YYYY-MM-DD).
328 pub date: String,
329 /// Human-readable title/headline.
330 pub title: String,
331 /// Optional summary/description.
332 pub summary: Option<String>,
333 /// Optional link URL for details.
334 pub url: Option<String>,
335 /// Source type (Arch news vs security advisory).
336 pub source: NewsFeedSource,
337 /// Optional advisory severity.
338 pub severity: Option<AdvisorySeverity>,
339 /// Affected packages (advisories only).
340 pub packages: Vec<String>,
341}
342
343/// What: Bundle of news feed items and associated last-seen state updates.
344///
345/// Inputs:
346/// - `items`: Aggregated news feed entries ready for rendering.
347/// - `seen_pkg_versions`: Updated map of installed package names to last-seen versions.
348/// - `seen_aur_comments`: Updated map of AUR packages to last-seen comment identifiers.
349///
350/// Output:
351/// - Carries feed payload plus dedupe state for persistence.
352///
353/// Details:
354/// - Used as the payload between background fetchers and UI to keep last-seen maps in sync.
355#[derive(Clone, Debug)]
356pub struct NewsFeedPayload {
357 /// Aggregated and sorted feed items.
358 pub items: Vec<NewsFeedItem>,
359 /// Last-seen versions for installed packages.
360 pub seen_pkg_versions: std::collections::HashMap<String, String>,
361 /// Last-seen comment identifiers for installed AUR packages.
362 pub seen_aur_comments: std::collections::HashMap<String, String>,
363}
364
365/// What: Persisted bookmark entry for news items, including cached content and optional local HTML path.
366///
367/// Inputs:
368/// - `item`: The news feed item metadata.
369/// - `content`: Parsed article content stored locally for offline display.
370/// - `html_path`: Optional filesystem path to the saved HTML file (if downloaded).
371#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
372pub struct NewsBookmark {
373 /// News feed metadata for the bookmark.
374 pub item: NewsFeedItem,
375 /// Parsed content cached locally.
376 pub content: Option<String>,
377 /// Path to the saved HTML file on disk (if downloaded).
378 pub html_path: Option<String>,
379}
380
381/// Package source origin.
382///
383/// Indicates whether a package originates from the official repositories or
384/// the Arch User Repository.
385#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
386pub enum Source {
387 /// Official repository package and its associated repository and target
388 /// architecture.
389 Official {
390 /// Repository name (e.g., "core", "extra", "community").
391 repo: String,
392 /// Target architecture (e.g., `x86_64`).
393 arch: String,
394 },
395 /// AUR package.
396 Aur,
397}
398
399/// Minimal package summary used in lists and search results.
400///
401/// This is compact enough to render in lists and panes. For a richer, detailed
402/// view, see [`PackageDetails`].
403#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
404pub struct PackageItem {
405 /// Canonical package name.
406 pub name: String,
407 /// Version string as reported by the source.
408 pub version: String,
409 /// One-line description suitable for list display.
410 pub description: String,
411 /// Origin of the package (official repo or AUR).
412 pub source: Source,
413 /// AUR popularity score when available (AUR only).
414 #[serde(default, skip_serializing_if = "Option::is_none")]
415 pub popularity: Option<f64>,
416 /// Timestamp when package was flagged out-of-date (AUR only).
417 #[serde(default, skip_serializing_if = "Option::is_none")]
418 pub out_of_date: Option<u64>,
419 /// Whether package is orphaned (no active maintainer) (AUR only).
420 #[serde(default, skip_serializing_if = "is_false")]
421 pub orphaned: bool,
422}
423
424/// Full set of details for a package, suitable for a dedicated information
425/// pane.
426#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
427pub struct PackageDetails {
428 /// Repository name (e.g., "extra").
429 pub repository: String,
430 /// Package name.
431 pub name: String,
432 /// Full version string.
433 pub version: String,
434 /// Long description.
435 pub description: String,
436 /// Target architecture.
437 pub architecture: String,
438 /// Upstream project URL (may be empty if unknown).
439 pub url: String,
440 /// SPDX or human-readable license identifiers.
441 pub licenses: Vec<String>,
442 /// Group memberships.
443 pub groups: Vec<String>,
444 /// Virtual provisions supplied by this package.
445 pub provides: Vec<String>,
446 /// Required dependencies.
447 pub depends: Vec<String>,
448 /// Optional dependencies with annotations.
449 pub opt_depends: Vec<String>,
450 /// Packages that require this package.
451 pub required_by: Vec<String>,
452 /// Packages for which this package is optional.
453 pub optional_for: Vec<String>,
454 /// Conflicting packages.
455 pub conflicts: Vec<String>,
456 /// Packages that this package replaces.
457 pub replaces: Vec<String>,
458 /// Download size in bytes, if available.
459 pub download_size: Option<u64>,
460 /// Installed size in bytes, if available.
461 pub install_size: Option<u64>,
462 /// Packager or maintainer name.
463 pub owner: String, // packager/maintainer
464 /// Build or packaging date (string-formatted for display).
465 pub build_date: String,
466 /// AUR popularity score when available (AUR only).
467 #[serde(default, skip_serializing_if = "Option::is_none")]
468 pub popularity: Option<f64>,
469 /// Timestamp when package was flagged out-of-date (AUR only).
470 #[serde(default, skip_serializing_if = "Option::is_none")]
471 pub out_of_date: Option<u64>,
472 /// Whether package is orphaned (no active maintainer) (AUR only).
473 #[serde(default, skip_serializing_if = "is_false")]
474 pub orphaned: bool,
475}
476
477/// Search query sent to the background search worker.
478#[derive(Clone, Debug)]
479pub struct QueryInput {
480 /// Monotonic identifier used to correlate responses.
481 pub id: u64,
482 /// Raw query text entered by the user.
483 pub text: String,
484 /// Whether fuzzy search mode is enabled.
485 pub fuzzy: bool,
486}
487
488/// Results corresponding to a prior [`QueryInput`].
489#[derive(Clone, Debug)]
490pub struct SearchResults {
491 /// Echoed identifier from the originating query.
492 pub id: u64,
493 /// Matching packages in rank order.
494 pub items: Vec<PackageItem>,
495}
496
497/// What: Request payload to run PKGBUILD static checks.
498#[derive(Clone, Debug)]
499pub struct PkgbuildCheckRequest {
500 /// Selected package name.
501 pub package_name: String,
502 /// Current PKGBUILD text shown in preview.
503 pub pkgbuild_text: String,
504 /// Global dry-run flag.
505 pub dry_run: bool,
506}
507
508/// What: Response payload for PKGBUILD static checks.
509#[derive(Clone, Debug)]
510pub struct PkgbuildCheckResponse {
511 /// Package name tied to this run.
512 pub package_name: String,
513 /// Parsed findings for list rendering.
514 pub findings: Vec<crate::state::app_state::PkgbuildCheckFinding>,
515 /// Raw per-tool outputs from latest PKGBUILD check run.
516 pub raw_results: Vec<crate::state::app_state::PkgbuildToolRawResult>,
517 /// User-facing missing tool hints.
518 pub missing_tools: Vec<String>,
519 /// Optional high-level execution error.
520 pub last_error: Option<String>,
521}
522
523/// Sorting mode for the Results list.
524#[derive(Debug, Clone, Copy, PartialEq, Eq)]
525pub enum SortMode {
526 /// Default: Pacman (core/extra/other official) first, then AUR; name tiebreak.
527 RepoThenName,
528 /// AUR first (by highest popularity), then official repos; name tiebreak.
529 AurPopularityThenOfficial,
530 /// Best matches: Relevance by name to current query, then repo order, then name.
531 BestMatches,
532}
533
534impl SortMode {
535 /// Return the string key used in settings files for this sort mode.
536 ///
537 /// What: Map the enum variant to its persisted configuration key.
538 /// - Input: None; uses the receiver variant.
539 /// - Output: Static string representing the serialized value.
540 /// - Details: Keeps `settings.conf` forward/backward compatible by
541 /// standardizing the keys stored on disk.
542 #[must_use]
543 pub const fn as_config_key(&self) -> &'static str {
544 match self {
545 Self::RepoThenName => "alphabetical",
546 Self::AurPopularityThenOfficial => "aur_popularity",
547 Self::BestMatches => "best_matches",
548 }
549 }
550 /// Parse a sort mode from its settings key or legacy aliases.
551 ///
552 /// What: Convert persisted config values back into `SortMode` variants.
553 /// - Input: `s` string slice containing the stored key (case-insensitive).
554 /// - Output: `Some(SortMode)` when a known variant matches; `None` for
555 /// unrecognized keys.
556 /// - Details: Accepts historical aliases to maintain compatibility with
557 /// earlier Pacsea releases.
558 #[must_use]
559 pub fn from_config_key(s: &str) -> Option<Self> {
560 match s.trim().to_lowercase().as_str() {
561 "alphabetical" | "repo_then_name" | "pacman" => Some(Self::RepoThenName),
562 "aur_popularity" | "popularity" => Some(Self::AurPopularityThenOfficial),
563 "best_matches" | "relevance" => Some(Self::BestMatches),
564 _ => None,
565 }
566 }
567}
568
569/// Filter mode for installed packages in the "Installed" toggle.
570///
571/// What: Controls which packages are shown when viewing installed packages.
572/// - `LeafOnly`: Show only explicitly installed packages with no dependents (pacman -Qetq).
573/// - `AllExplicit`: Show all explicitly installed packages (pacman -Qeq).
574///
575/// Details:
576/// - `LeafOnly` is the default, showing packages safe to remove.
577/// - `AllExplicit` includes packages that other packages depend on.
578#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
579pub enum InstalledPackagesMode {
580 /// Show only leaf packages (explicitly installed, nothing depends on them).
581 #[default]
582 LeafOnly,
583 /// Show all explicitly installed packages.
584 AllExplicit,
585}
586
587impl InstalledPackagesMode {
588 /// Return the string key used in settings files for this mode.
589 ///
590 /// What: Map the enum variant to its persisted configuration key.
591 /// - Input: None; uses the receiver variant.
592 /// - Output: Static string representing the serialized value.
593 #[must_use]
594 pub const fn as_config_key(&self) -> &'static str {
595 match self {
596 Self::LeafOnly => "leaf",
597 Self::AllExplicit => "all",
598 }
599 }
600
601 /// Parse an installed packages mode from its settings key.
602 ///
603 /// What: Convert persisted config values back into `InstalledPackagesMode` variants.
604 /// - Input: `s` string slice containing the stored key (case-insensitive).
605 /// - Output: `Some(InstalledPackagesMode)` when a known variant matches; `None` otherwise.
606 #[must_use]
607 pub fn from_config_key(s: &str) -> Option<Self> {
608 match s.trim().to_lowercase().as_str() {
609 "leaf" | "leaf_only" => Some(Self::LeafOnly),
610 "all" | "all_explicit" => Some(Self::AllExplicit),
611 _ => None,
612 }
613 }
614}
615
616#[cfg(test)]
617mod tests {
618 use super::{InstalledPackagesMode, SortMode};
619
620 #[test]
621 /// What: Validate `SortMode` converts to and from configuration keys, including legacy aliases.
622 ///
623 /// Inputs:
624 /// - Known config keys, historical aliases, and a deliberately unknown key.
625 ///
626 /// Output:
627 /// - Returns the expected enum variants for recognised keys and `None` for the unknown entry.
628 ///
629 /// Details:
630 /// - Guards against accidental regressions when tweaking the accepted key list or canonical names.
631 fn state_sortmode_config_roundtrip_and_aliases() {
632 assert_eq!(SortMode::RepoThenName.as_config_key(), "alphabetical");
633 assert_eq!(
634 SortMode::from_config_key("alphabetical"),
635 Some(SortMode::RepoThenName)
636 );
637 assert_eq!(
638 SortMode::from_config_key("repo_then_name"),
639 Some(SortMode::RepoThenName)
640 );
641 assert_eq!(
642 SortMode::from_config_key("pacman"),
643 Some(SortMode::RepoThenName)
644 );
645 assert_eq!(
646 SortMode::from_config_key("aur_popularity"),
647 Some(SortMode::AurPopularityThenOfficial)
648 );
649 assert_eq!(
650 SortMode::from_config_key("popularity"),
651 Some(SortMode::AurPopularityThenOfficial)
652 );
653 assert_eq!(
654 SortMode::from_config_key("best_matches"),
655 Some(SortMode::BestMatches)
656 );
657 assert_eq!(
658 SortMode::from_config_key("relevance"),
659 Some(SortMode::BestMatches)
660 );
661 assert_eq!(SortMode::from_config_key("unknown"), None);
662 }
663
664 #[test]
665 /// What: Validate `InstalledPackagesMode` converts to and from configuration keys, including aliases.
666 ///
667 /// Inputs:
668 /// - Known config keys, aliases, case variations, whitespace, and a deliberately unknown key.
669 ///
670 /// Output:
671 /// - Returns the expected enum variants for recognised keys and `None` for the unknown entry.
672 ///
673 /// Details:
674 /// - Guards against accidental regressions when tweaking the accepted key list or canonical names.
675 /// - Verifies roundtrip conversions and case-insensitive parsing.
676 fn state_installedpackagesmode_config_roundtrip_and_aliases() {
677 // Test as_config_key for both variants
678 assert_eq!(InstalledPackagesMode::LeafOnly.as_config_key(), "leaf");
679 assert_eq!(InstalledPackagesMode::AllExplicit.as_config_key(), "all");
680
681 // Test from_config_key with canonical keys
682 assert_eq!(
683 InstalledPackagesMode::from_config_key("leaf"),
684 Some(InstalledPackagesMode::LeafOnly)
685 );
686 assert_eq!(
687 InstalledPackagesMode::from_config_key("all"),
688 Some(InstalledPackagesMode::AllExplicit)
689 );
690
691 // Test from_config_key with aliases
692 assert_eq!(
693 InstalledPackagesMode::from_config_key("leaf_only"),
694 Some(InstalledPackagesMode::LeafOnly)
695 );
696 assert_eq!(
697 InstalledPackagesMode::from_config_key("all_explicit"),
698 Some(InstalledPackagesMode::AllExplicit)
699 );
700
701 // Test roundtrip conversions
702 assert_eq!(
703 InstalledPackagesMode::from_config_key(InstalledPackagesMode::LeafOnly.as_config_key()),
704 Some(InstalledPackagesMode::LeafOnly)
705 );
706 assert_eq!(
707 InstalledPackagesMode::from_config_key(
708 InstalledPackagesMode::AllExplicit.as_config_key()
709 ),
710 Some(InstalledPackagesMode::AllExplicit)
711 );
712
713 // Test case insensitivity
714 assert_eq!(
715 InstalledPackagesMode::from_config_key("LEAF"),
716 Some(InstalledPackagesMode::LeafOnly)
717 );
718 assert_eq!(
719 InstalledPackagesMode::from_config_key("Leaf"),
720 Some(InstalledPackagesMode::LeafOnly)
721 );
722 assert_eq!(
723 InstalledPackagesMode::from_config_key("LEAF_ONLY"),
724 Some(InstalledPackagesMode::LeafOnly)
725 );
726 assert_eq!(
727 InstalledPackagesMode::from_config_key("All"),
728 Some(InstalledPackagesMode::AllExplicit)
729 );
730 assert_eq!(
731 InstalledPackagesMode::from_config_key("ALL_EXPLICIT"),
732 Some(InstalledPackagesMode::AllExplicit)
733 );
734
735 // Test whitespace trimming
736 assert_eq!(
737 InstalledPackagesMode::from_config_key(" leaf "),
738 Some(InstalledPackagesMode::LeafOnly)
739 );
740 assert_eq!(
741 InstalledPackagesMode::from_config_key(" all "),
742 Some(InstalledPackagesMode::AllExplicit)
743 );
744 assert_eq!(
745 InstalledPackagesMode::from_config_key(" leaf_only "),
746 Some(InstalledPackagesMode::LeafOnly)
747 );
748 assert_eq!(
749 InstalledPackagesMode::from_config_key(" all_explicit "),
750 Some(InstalledPackagesMode::AllExplicit)
751 );
752
753 // Test unknown key
754 assert_eq!(InstalledPackagesMode::from_config_key("unknown"), None);
755 assert_eq!(InstalledPackagesMode::from_config_key(""), None);
756 }
757}
758
759/// Visual indicator for Arch status line.
760#[derive(Debug, Clone, Copy, PartialEq, Eq)]
761pub enum ArchStatusColor {
762 /// No color known yet.
763 None,
764 /// Everything operational (green).
765 Operational,
766 /// Relevant incident today (yellow).
767 IncidentToday,
768 /// Severe incident today (red).
769 IncidentSevereToday,
770}
771
772/// Which UI pane currently has keyboard focus.
773#[derive(Debug, Clone, Copy, PartialEq, Eq)]
774pub enum Focus {
775 /// Center pane: search input and results.
776 Search,
777 /// Left pane: recent queries list.
778 Recent,
779 /// Right pane: pending install list.
780 Install,
781}
782
783/// Which sub-pane within the right column is currently focused when applicable.
784#[derive(Debug, Clone, Copy, PartialEq, Eq)]
785pub enum RightPaneFocus {
786 /// Normal mode: single Install list occupies the right column.
787 Install,
788 /// Installed-only mode: left subpane for planned downgrades.
789 Downgrade,
790 /// Installed-only mode: right subpane for removals.
791 Remove,
792}
793
794/// Row model for the "TUI Optional Deps" modal/list.
795/// Each row represents a concrete package candidate such as an editor,
796/// terminal, clipboard tool, mirror updater, or AUR helper.
797#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
798pub struct OptionalDepRow {
799 /// Human-friendly label to display in the UI (e.g., "Editor: nvim", "Terminal: kitty").
800 pub label: String,
801 /// The concrete package name to check/install (e.g., "nvim", "kitty", "wl-clipboard",
802 /// "reflector", "pacman-mirrors", "paru", "yay").
803 pub package: String,
804 /// Whether this dependency is currently installed on the system.
805 #[serde(default)]
806 pub installed: bool,
807 /// Whether the user can select this row for installation (only when not installed).
808 #[serde(default)]
809 pub selectable: bool,
810 /// Optional note for environment/distro constraints (e.g., "Wayland", "X11", "Manjaro only").
811 #[serde(default, skip_serializing_if = "Option::is_none")]
812 pub note: Option<String>,
813}
814
815/// What: Pacman `[repo]` presence as shown in the read-only Repositories modal.
816///
817/// Inputs:
818/// - Set when merging `repos.conf` rows with a live `pacman.conf` scan.
819///
820/// Output:
821/// - Drives result column labels in the UI.
822///
823/// Details:
824/// - Distinct from Pacsea results-filter toggles; this reflects `/etc/pacman.conf` only.
825#[derive(Clone, Copy, Debug, PartialEq, Eq)]
826pub enum RepositoryPacmanStatus {
827 /// No matching section header found.
828 Absent,
829 /// Active `[name]` header exists.
830 Active,
831 /// Only `# [name]` (commented) headers exist.
832 Commented,
833}
834
835/// What: Signing key trust hint for a `[[repo]]` row that declares `key_id`.
836///
837/// Inputs:
838/// - Derived from a batched `pacman-key --list-keys` check.
839///
840/// Output:
841/// - Column text in the Repositories modal.
842///
843/// Details:
844/// - `Unknown` covers missing `pacman-key`, failed runs, or fingerprints too short to match safely.
845#[derive(Clone, Copy, Debug, PartialEq, Eq)]
846pub enum RepositoryKeyTrust {
847 /// Row has no `key_id`; nothing to verify.
848 NotApplicable,
849 /// Fingerprint (normalized) appears in the key listing.
850 Trusted,
851 /// Listing succeeded but fingerprint not found.
852 NotTrusted,
853 /// Could not determine (tool missing, error, or invalid id).
854 Unknown,
855}
856
857/// What: One row in the read-only Repositories modal (merged `repos.conf` + live pacman scan).
858///
859/// Inputs:
860/// - Built when opening the Repositories modal from `logic::repos`.
861///
862/// Output:
863/// - Rendered as a list line with status chips.
864///
865/// Details:
866/// - Read-only in Phase 2; apply flows will extend behavior later.
867#[derive(Clone, Debug)]
868pub struct RepositoryModalRow {
869 /// Pacman section `name` from `repos.conf`.
870 pub pacman_section_name: String,
871 /// Raw `results_filter` label for display.
872 pub results_filter_display: String,
873 /// Whether `/etc/pacman.conf` (includes) contains this repo section.
874 pub pacman_status: RepositoryPacmanStatus,
875 /// Optional short source file hint (e.g. include file name).
876 pub source_hint: Option<String>,
877 /// Keyring trust classification when `key_id` is set.
878 pub key_trust: RepositoryKeyTrust,
879}
880
881/// AUR package comment data structure.
882///
883/// What: Represents a single comment from an AUR package page.
884///
885/// Inputs: None (data structure).
886///
887/// Output: None (data structure).
888///
889/// Details:
890/// - Contains author, date, and content of a comment.
891/// - Includes optional timestamp for reliable chronological sorting.
892#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
893pub struct AurComment {
894 /// Stable comment identifier parsed from DOM when available.
895 #[serde(default, skip_serializing_if = "Option::is_none")]
896 pub id: Option<String>,
897 /// Comment author username.
898 pub author: String,
899 /// Human-readable date string.
900 pub date: String,
901 /// Unix timestamp for sorting (None if parsing failed).
902 #[serde(default, skip_serializing_if = "Option::is_none")]
903 pub date_timestamp: Option<i64>,
904 /// URL from the date link (None if not available).
905 #[serde(default, skip_serializing_if = "Option::is_none")]
906 pub date_url: Option<String>,
907 /// Comment content text.
908 pub content: String,
909 /// Whether this comment is pinned (shown at the top).
910 #[serde(default)]
911 pub pinned: bool,
912}
913
914/// Helper function for serde to skip serializing false boolean values.
915#[allow(clippy::trivially_copy_pass_by_ref)]
916const fn is_false(b: &bool) -> bool {
917 !(*b)
918}