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