pacsea/state/
types.rs

1//! Core value types used by Pacsea state.
2
3/// Minimal news entry for Arch news modal.
4#[derive(Clone, Debug)]
5pub struct NewsItem {
6    /// Publication date (short, e.g., 2025-10-11)
7    pub date: String,
8    /// Title text
9    pub title: String,
10    /// Link URL
11    pub url: String,
12}
13
14/// What: High-level application mode.
15///
16/// Inputs: None (enum variants)
17///
18/// Output: Represents whether the UI is in package management or news view.
19///
20/// Details:
21/// - `Package` preserves the existing package management experience.
22/// - `News` switches panes to the news feed experience.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum AppMode {
25    /// Package management/search mode (existing UI).
26    Package,
27    /// News feed mode (new UI).
28    News,
29}
30
31/// What: News/advisory source type.
32///
33/// Inputs: None (enum variants)
34///
35/// Output: Identifies where a news feed item originates.
36///
37/// Details:
38/// - Distinguishes Arch news RSS posts from security advisories.
39#[derive(
40    Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
41)]
42pub enum NewsFeedSource {
43    /// Official Arch Linux news RSS item.
44    ArchNews,
45    /// security.archlinux.org advisory.
46    SecurityAdvisory,
47    /// Installed official package received a version update.
48    InstalledPackageUpdate,
49    /// Installed AUR package received a version update.
50    AurPackageUpdate,
51    /// New AUR comment on an installed package.
52    AurComment,
53}
54
55/// What: Severity levels for security advisories.
56///
57/// Inputs: None (enum variants)
58///
59/// Output: Normalized advisory severity.
60///
61/// Details:
62/// - Ordered from lowest to highest severity for sorting.
63#[derive(
64    Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
65)]
66pub enum AdvisorySeverity {
67    /// Unknown or not provided.
68    Unknown,
69    /// Low severity.
70    Low,
71    /// Medium severity.
72    Medium,
73    /// High severity.
74    High,
75    /// Critical severity.
76    Critical,
77}
78
79/// What: Map advisory severity to a numeric rank for sorting (higher is worse).
80///
81/// Inputs:
82/// - `severity`: Optional advisory severity value.
83///
84/// Output:
85/// - Numeric rank where larger numbers indicate higher severity (Critical highest).
86///
87/// Details:
88/// - Returns `0` when severity is missing to ensure advisories without severity fall last.
89/// - Keeps ordering stable across both news feed sorting and advisory-specific listings.
90#[must_use]
91pub const fn severity_rank(severity: Option<AdvisorySeverity>) -> u8 {
92    match severity {
93        Some(AdvisorySeverity::Critical) => 5,
94        Some(AdvisorySeverity::High) => 4,
95        Some(AdvisorySeverity::Medium) => 3,
96        Some(AdvisorySeverity::Low) => 2,
97        Some(AdvisorySeverity::Unknown) => 1,
98        None => 0,
99    }
100}
101
102/// What: Sort options for news feed results.
103///
104/// Inputs: None (enum variants)
105///
106/// Output: Selected sort mode for news items.
107///
108/// Details:
109/// - `DateDesc` is newest-first default.
110#[derive(Clone, Copy, Debug, PartialEq, Eq)]
111pub enum NewsSortMode {
112    /// Newest first by date.
113    DateDesc,
114    /// Oldest first by date.
115    DateAsc,
116    /// Alphabetical by title.
117    Title,
118    /// Group by source then title.
119    SourceThenTitle,
120    /// Severity first (Critical..Unknown), then date (newest first).
121    SeverityThenDate,
122    /// Unread items first, then date (newest first).
123    UnreadThenDate,
124}
125
126/// What: Read filter applied to news feed items.
127///
128/// Inputs: None (enum variants)
129///
130/// Output:
131/// - Indicates whether to show all, only read, or only unread items.
132///
133/// Details:
134/// - Used by the News Feed list and toolbar filter chip.
135#[derive(Clone, Copy, Debug, PartialEq, Eq)]
136pub enum NewsReadFilter {
137    /// Show all items regardless of read status.
138    All,
139    /// Show only items marked as read.
140    Read,
141    /// Show only items not marked as read.
142    Unread,
143}
144
145/// What: Unified news/advisory feed item for the news view.
146///
147/// Inputs:
148/// - Fields describing the item (title, summary, url, source, severity, packages, date)
149///
150/// Output:
151/// - Data ready for list and details rendering in news mode.
152///
153/// Details:
154/// - `id` is a stable identifier (URL for news, advisory ID for security).
155/// - `packages` holds affected package names for advisories.
156#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
157pub struct NewsFeedItem {
158    /// Stable identifier (URL or advisory ID).
159    pub id: String,
160    /// Publication or update date (YYYY-MM-DD).
161    pub date: String,
162    /// Human-readable title/headline.
163    pub title: String,
164    /// Optional summary/description.
165    pub summary: Option<String>,
166    /// Optional link URL for details.
167    pub url: Option<String>,
168    /// Source type (Arch news vs security advisory).
169    pub source: NewsFeedSource,
170    /// Optional advisory severity.
171    pub severity: Option<AdvisorySeverity>,
172    /// Affected packages (advisories only).
173    pub packages: Vec<String>,
174}
175
176/// What: Bundle of news feed items and associated last-seen state updates.
177///
178/// Inputs:
179/// - `items`: Aggregated news feed entries ready for rendering.
180/// - `seen_pkg_versions`: Updated map of installed package names to last-seen versions.
181/// - `seen_aur_comments`: Updated map of AUR packages to last-seen comment identifiers.
182///
183/// Output:
184/// - Carries feed payload plus dedupe state for persistence.
185///
186/// Details:
187/// - Used as the payload between background fetchers and UI to keep last-seen maps in sync.
188#[derive(Clone, Debug)]
189pub struct NewsFeedPayload {
190    /// Aggregated and sorted feed items.
191    pub items: Vec<NewsFeedItem>,
192    /// Last-seen versions for installed packages.
193    pub seen_pkg_versions: std::collections::HashMap<String, String>,
194    /// Last-seen comment identifiers for installed AUR packages.
195    pub seen_aur_comments: std::collections::HashMap<String, String>,
196}
197
198/// What: Persisted bookmark entry for news items, including cached content and optional local HTML path.
199///
200/// Inputs:
201/// - `item`: The news feed item metadata.
202/// - `content`: Parsed article content stored locally for offline display.
203/// - `html_path`: Optional filesystem path to the saved HTML file (if downloaded).
204#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
205pub struct NewsBookmark {
206    /// News feed metadata for the bookmark.
207    pub item: NewsFeedItem,
208    /// Parsed content cached locally.
209    pub content: Option<String>,
210    /// Path to the saved HTML file on disk (if downloaded).
211    pub html_path: Option<String>,
212}
213
214/// Package source origin.
215///
216/// Indicates whether a package originates from the official repositories or
217/// the Arch User Repository.
218#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
219pub enum Source {
220    /// Official repository package and its associated repository and target
221    /// architecture.
222    Official {
223        /// Repository name (e.g., "core", "extra", "community").
224        repo: String,
225        /// Target architecture (e.g., `x86_64`).
226        arch: String,
227    },
228    /// AUR package.
229    Aur,
230}
231
232/// Minimal package summary used in lists and search results.
233///
234/// This is compact enough to render in lists and panes. For a richer, detailed
235/// view, see [`PackageDetails`].
236#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
237pub struct PackageItem {
238    /// Canonical package name.
239    pub name: String,
240    /// Version string as reported by the source.
241    pub version: String,
242    /// One-line description suitable for list display.
243    pub description: String,
244    /// Origin of the package (official repo or AUR).
245    pub source: Source,
246    /// AUR popularity score when available (AUR only).
247    #[serde(default, skip_serializing_if = "Option::is_none")]
248    pub popularity: Option<f64>,
249    /// Timestamp when package was flagged out-of-date (AUR only).
250    #[serde(default, skip_serializing_if = "Option::is_none")]
251    pub out_of_date: Option<u64>,
252    /// Whether package is orphaned (no active maintainer) (AUR only).
253    #[serde(default, skip_serializing_if = "is_false")]
254    pub orphaned: bool,
255}
256
257/// Full set of details for a package, suitable for a dedicated information
258/// pane.
259#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
260pub struct PackageDetails {
261    /// Repository name (e.g., "extra").
262    pub repository: String,
263    /// Package name.
264    pub name: String,
265    /// Full version string.
266    pub version: String,
267    /// Long description.
268    pub description: String,
269    /// Target architecture.
270    pub architecture: String,
271    /// Upstream project URL (may be empty if unknown).
272    pub url: String,
273    /// SPDX or human-readable license identifiers.
274    pub licenses: Vec<String>,
275    /// Group memberships.
276    pub groups: Vec<String>,
277    /// Virtual provisions supplied by this package.
278    pub provides: Vec<String>,
279    /// Required dependencies.
280    pub depends: Vec<String>,
281    /// Optional dependencies with annotations.
282    pub opt_depends: Vec<String>,
283    /// Packages that require this package.
284    pub required_by: Vec<String>,
285    /// Packages for which this package is optional.
286    pub optional_for: Vec<String>,
287    /// Conflicting packages.
288    pub conflicts: Vec<String>,
289    /// Packages that this package replaces.
290    pub replaces: Vec<String>,
291    /// Download size in bytes, if available.
292    pub download_size: Option<u64>,
293    /// Installed size in bytes, if available.
294    pub install_size: Option<u64>,
295    /// Packager or maintainer name.
296    pub owner: String, // packager/maintainer
297    /// Build or packaging date (string-formatted for display).
298    pub build_date: String,
299    /// AUR popularity score when available (AUR only).
300    #[serde(default, skip_serializing_if = "Option::is_none")]
301    pub popularity: Option<f64>,
302    /// Timestamp when package was flagged out-of-date (AUR only).
303    #[serde(default, skip_serializing_if = "Option::is_none")]
304    pub out_of_date: Option<u64>,
305    /// Whether package is orphaned (no active maintainer) (AUR only).
306    #[serde(default, skip_serializing_if = "is_false")]
307    pub orphaned: bool,
308}
309
310/// Search query sent to the background search worker.
311#[derive(Clone, Debug)]
312pub struct QueryInput {
313    /// Monotonic identifier used to correlate responses.
314    pub id: u64,
315    /// Raw query text entered by the user.
316    pub text: String,
317    /// Whether fuzzy search mode is enabled.
318    pub fuzzy: bool,
319}
320
321/// Results corresponding to a prior [`QueryInput`].
322#[derive(Clone, Debug)]
323pub struct SearchResults {
324    /// Echoed identifier from the originating query.
325    pub id: u64,
326    /// Matching packages in rank order.
327    pub items: Vec<PackageItem>,
328}
329
330/// Sorting mode for the Results list.
331#[derive(Debug, Clone, Copy, PartialEq, Eq)]
332pub enum SortMode {
333    /// Default: Pacman (core/extra/other official) first, then AUR; name tiebreak.
334    RepoThenName,
335    /// AUR first (by highest popularity), then official repos; name tiebreak.
336    AurPopularityThenOfficial,
337    /// Best matches: Relevance by name to current query, then repo order, then name.
338    BestMatches,
339}
340
341impl SortMode {
342    /// Return the string key used in settings files for this sort mode.
343    ///
344    /// What: Map the enum variant to its persisted configuration key.
345    /// - Input: None; uses the receiver variant.
346    /// - Output: Static string representing the serialized value.
347    /// - Details: Keeps `settings.conf` forward/backward compatible by
348    ///   standardizing the keys stored on disk.
349    #[must_use]
350    pub const fn as_config_key(&self) -> &'static str {
351        match self {
352            Self::RepoThenName => "alphabetical",
353            Self::AurPopularityThenOfficial => "aur_popularity",
354            Self::BestMatches => "best_matches",
355        }
356    }
357    /// Parse a sort mode from its settings key or legacy aliases.
358    ///
359    /// What: Convert persisted config values back into `SortMode` variants.
360    /// - Input: `s` string slice containing the stored key (case-insensitive).
361    /// - Output: `Some(SortMode)` when a known variant matches; `None` for
362    ///   unrecognized keys.
363    /// - Details: Accepts historical aliases to maintain compatibility with
364    ///   earlier Pacsea releases.
365    #[must_use]
366    pub fn from_config_key(s: &str) -> Option<Self> {
367        match s.trim().to_lowercase().as_str() {
368            "alphabetical" | "repo_then_name" | "pacman" => Some(Self::RepoThenName),
369            "aur_popularity" | "popularity" => Some(Self::AurPopularityThenOfficial),
370            "best_matches" | "relevance" => Some(Self::BestMatches),
371            _ => None,
372        }
373    }
374}
375
376/// Filter mode for installed packages in the "Installed" toggle.
377///
378/// What: Controls which packages are shown when viewing installed packages.
379/// - `LeafOnly`: Show only explicitly installed packages with no dependents (pacman -Qetq).
380/// - `AllExplicit`: Show all explicitly installed packages (pacman -Qeq).
381///
382/// Details:
383/// - `LeafOnly` is the default, showing packages safe to remove.
384/// - `AllExplicit` includes packages that other packages depend on.
385#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
386pub enum InstalledPackagesMode {
387    /// Show only leaf packages (explicitly installed, nothing depends on them).
388    #[default]
389    LeafOnly,
390    /// Show all explicitly installed packages.
391    AllExplicit,
392}
393
394impl InstalledPackagesMode {
395    /// Return the string key used in settings files for this mode.
396    ///
397    /// What: Map the enum variant to its persisted configuration key.
398    /// - Input: None; uses the receiver variant.
399    /// - Output: Static string representing the serialized value.
400    #[must_use]
401    pub const fn as_config_key(&self) -> &'static str {
402        match self {
403            Self::LeafOnly => "leaf",
404            Self::AllExplicit => "all",
405        }
406    }
407
408    /// Parse an installed packages mode from its settings key.
409    ///
410    /// What: Convert persisted config values back into `InstalledPackagesMode` variants.
411    /// - Input: `s` string slice containing the stored key (case-insensitive).
412    /// - Output: `Some(InstalledPackagesMode)` when a known variant matches; `None` otherwise.
413    #[must_use]
414    pub fn from_config_key(s: &str) -> Option<Self> {
415        match s.trim().to_lowercase().as_str() {
416            "leaf" | "leaf_only" => Some(Self::LeafOnly),
417            "all" | "all_explicit" => Some(Self::AllExplicit),
418            _ => None,
419        }
420    }
421}
422
423#[cfg(test)]
424mod tests {
425    use super::{InstalledPackagesMode, SortMode};
426
427    #[test]
428    /// What: Validate `SortMode` converts to and from configuration keys, including legacy aliases.
429    ///
430    /// Inputs:
431    /// - Known config keys, historical aliases, and a deliberately unknown key.
432    ///
433    /// Output:
434    /// - Returns the expected enum variants for recognised keys and `None` for the unknown entry.
435    ///
436    /// Details:
437    /// - Guards against accidental regressions when tweaking the accepted key list or canonical names.
438    fn state_sortmode_config_roundtrip_and_aliases() {
439        assert_eq!(SortMode::RepoThenName.as_config_key(), "alphabetical");
440        assert_eq!(
441            SortMode::from_config_key("alphabetical"),
442            Some(SortMode::RepoThenName)
443        );
444        assert_eq!(
445            SortMode::from_config_key("repo_then_name"),
446            Some(SortMode::RepoThenName)
447        );
448        assert_eq!(
449            SortMode::from_config_key("pacman"),
450            Some(SortMode::RepoThenName)
451        );
452        assert_eq!(
453            SortMode::from_config_key("aur_popularity"),
454            Some(SortMode::AurPopularityThenOfficial)
455        );
456        assert_eq!(
457            SortMode::from_config_key("popularity"),
458            Some(SortMode::AurPopularityThenOfficial)
459        );
460        assert_eq!(
461            SortMode::from_config_key("best_matches"),
462            Some(SortMode::BestMatches)
463        );
464        assert_eq!(
465            SortMode::from_config_key("relevance"),
466            Some(SortMode::BestMatches)
467        );
468        assert_eq!(SortMode::from_config_key("unknown"), None);
469    }
470
471    #[test]
472    /// What: Validate `InstalledPackagesMode` converts to and from configuration keys, including aliases.
473    ///
474    /// Inputs:
475    /// - Known config keys, aliases, case variations, whitespace, and a deliberately unknown key.
476    ///
477    /// Output:
478    /// - Returns the expected enum variants for recognised keys and `None` for the unknown entry.
479    ///
480    /// Details:
481    /// - Guards against accidental regressions when tweaking the accepted key list or canonical names.
482    /// - Verifies roundtrip conversions and case-insensitive parsing.
483    fn state_installedpackagesmode_config_roundtrip_and_aliases() {
484        // Test as_config_key for both variants
485        assert_eq!(InstalledPackagesMode::LeafOnly.as_config_key(), "leaf");
486        assert_eq!(InstalledPackagesMode::AllExplicit.as_config_key(), "all");
487
488        // Test from_config_key with canonical keys
489        assert_eq!(
490            InstalledPackagesMode::from_config_key("leaf"),
491            Some(InstalledPackagesMode::LeafOnly)
492        );
493        assert_eq!(
494            InstalledPackagesMode::from_config_key("all"),
495            Some(InstalledPackagesMode::AllExplicit)
496        );
497
498        // Test from_config_key with aliases
499        assert_eq!(
500            InstalledPackagesMode::from_config_key("leaf_only"),
501            Some(InstalledPackagesMode::LeafOnly)
502        );
503        assert_eq!(
504            InstalledPackagesMode::from_config_key("all_explicit"),
505            Some(InstalledPackagesMode::AllExplicit)
506        );
507
508        // Test roundtrip conversions
509        assert_eq!(
510            InstalledPackagesMode::from_config_key(InstalledPackagesMode::LeafOnly.as_config_key()),
511            Some(InstalledPackagesMode::LeafOnly)
512        );
513        assert_eq!(
514            InstalledPackagesMode::from_config_key(
515                InstalledPackagesMode::AllExplicit.as_config_key()
516            ),
517            Some(InstalledPackagesMode::AllExplicit)
518        );
519
520        // Test case insensitivity
521        assert_eq!(
522            InstalledPackagesMode::from_config_key("LEAF"),
523            Some(InstalledPackagesMode::LeafOnly)
524        );
525        assert_eq!(
526            InstalledPackagesMode::from_config_key("Leaf"),
527            Some(InstalledPackagesMode::LeafOnly)
528        );
529        assert_eq!(
530            InstalledPackagesMode::from_config_key("LEAF_ONLY"),
531            Some(InstalledPackagesMode::LeafOnly)
532        );
533        assert_eq!(
534            InstalledPackagesMode::from_config_key("All"),
535            Some(InstalledPackagesMode::AllExplicit)
536        );
537        assert_eq!(
538            InstalledPackagesMode::from_config_key("ALL_EXPLICIT"),
539            Some(InstalledPackagesMode::AllExplicit)
540        );
541
542        // Test whitespace trimming
543        assert_eq!(
544            InstalledPackagesMode::from_config_key("  leaf  "),
545            Some(InstalledPackagesMode::LeafOnly)
546        );
547        assert_eq!(
548            InstalledPackagesMode::from_config_key("  all  "),
549            Some(InstalledPackagesMode::AllExplicit)
550        );
551        assert_eq!(
552            InstalledPackagesMode::from_config_key("  leaf_only  "),
553            Some(InstalledPackagesMode::LeafOnly)
554        );
555        assert_eq!(
556            InstalledPackagesMode::from_config_key("  all_explicit  "),
557            Some(InstalledPackagesMode::AllExplicit)
558        );
559
560        // Test unknown key
561        assert_eq!(InstalledPackagesMode::from_config_key("unknown"), None);
562        assert_eq!(InstalledPackagesMode::from_config_key(""), None);
563    }
564}
565
566/// Visual indicator for Arch status line.
567#[derive(Debug, Clone, Copy, PartialEq, Eq)]
568pub enum ArchStatusColor {
569    /// No color known yet.
570    None,
571    /// Everything operational (green).
572    Operational,
573    /// Relevant incident today (yellow).
574    IncidentToday,
575    /// Severe incident today (red).
576    IncidentSevereToday,
577}
578
579/// Which UI pane currently has keyboard focus.
580#[derive(Debug, Clone, Copy, PartialEq, Eq)]
581pub enum Focus {
582    /// Center pane: search input and results.
583    Search,
584    /// Left pane: recent queries list.
585    Recent,
586    /// Right pane: pending install list.
587    Install,
588}
589
590/// Which sub-pane within the right column is currently focused when applicable.
591#[derive(Debug, Clone, Copy, PartialEq, Eq)]
592pub enum RightPaneFocus {
593    /// Normal mode: single Install list occupies the right column.
594    Install,
595    /// Installed-only mode: left subpane for planned downgrades.
596    Downgrade,
597    /// Installed-only mode: right subpane for removals.
598    Remove,
599}
600
601/// Row model for the "TUI Optional Deps" modal/list.
602/// Each row represents a concrete package candidate such as an editor,
603/// terminal, clipboard tool, mirror updater, or AUR helper.
604#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
605pub struct OptionalDepRow {
606    /// Human-friendly label to display in the UI (e.g., "Editor: nvim", "Terminal: kitty").
607    pub label: String,
608    /// The concrete package name to check/install (e.g., "nvim", "kitty", "wl-clipboard",
609    /// "reflector", "pacman-mirrors", "paru", "yay").
610    pub package: String,
611    /// Whether this dependency is currently installed on the system.
612    #[serde(default)]
613    pub installed: bool,
614    /// Whether the user can select this row for installation (only when not installed).
615    #[serde(default)]
616    pub selectable: bool,
617    /// Optional note for environment/distro constraints (e.g., "Wayland", "X11", "Manjaro only").
618    #[serde(default, skip_serializing_if = "Option::is_none")]
619    pub note: Option<String>,
620}
621
622/// AUR package comment data structure.
623///
624/// What: Represents a single comment from an AUR package page.
625///
626/// Inputs: None (data structure).
627///
628/// Output: None (data structure).
629///
630/// Details:
631/// - Contains author, date, and content of a comment.
632/// - Includes optional timestamp for reliable chronological sorting.
633#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
634pub struct AurComment {
635    /// Stable comment identifier parsed from DOM when available.
636    #[serde(default, skip_serializing_if = "Option::is_none")]
637    pub id: Option<String>,
638    /// Comment author username.
639    pub author: String,
640    /// Human-readable date string.
641    pub date: String,
642    /// Unix timestamp for sorting (None if parsing failed).
643    #[serde(default, skip_serializing_if = "Option::is_none")]
644    pub date_timestamp: Option<i64>,
645    /// URL from the date link (None if not available).
646    #[serde(default, skip_serializing_if = "Option::is_none")]
647    pub date_url: Option<String>,
648    /// Comment content text.
649    pub content: String,
650    /// Whether this comment is pinned (shown at the top).
651    #[serde(default)]
652    pub pinned: bool,
653}
654
655/// Helper function for serde to skip serializing false boolean values.
656#[allow(clippy::trivially_copy_pass_by_ref)]
657const fn is_false(b: &bool) -> bool {
658    !(*b)
659}