Skip to main content

stout_state/
tap.rs

1//! Tap management for custom formula repositories
2
3use crate::error::Result;
4use crate::paths::Paths;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8/// A tap (custom formula repository)
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Tap {
11    /// Tap name (e.g., "user/repo")
12    pub name: String,
13    /// Base URL for the tap's index
14    pub url: String,
15    /// Whether this tap is pinned (prevents updates)
16    #[serde(default)]
17    pub pinned: bool,
18}
19
20/// Manages installed taps
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct TapManager {
23    #[serde(default)]
24    taps: HashMap<String, Tap>,
25}
26
27impl Default for TapManager {
28    fn default() -> Self {
29        let mut taps = HashMap::new();
30
31        // Default tap is the stout-index
32        taps.insert(
33            "neul-labs/stout-index".to_string(),
34            Tap {
35                name: "neul-labs/stout-index".to_string(),
36                url: "https://raw.githubusercontent.com/neul-labs/stout-index/main".to_string(),
37                pinned: false,
38            },
39        );
40
41        Self { taps }
42    }
43}
44
45impl TapManager {
46    /// Load taps from file
47    pub fn load(paths: &Paths) -> Result<Self> {
48        let taps_file = paths.stout_dir.join("taps.toml");
49
50        if taps_file.exists() {
51            let contents = std::fs::read_to_string(&taps_file)?;
52            let manager: TapManager = toml::from_str(&contents)?;
53            Ok(manager)
54        } else {
55            Ok(Self::default())
56        }
57    }
58
59    /// Save taps to file
60    pub fn save(&self, paths: &Paths) -> Result<()> {
61        let taps_file = paths.stout_dir.join("taps.toml");
62
63        if let Some(parent) = taps_file.parent() {
64            std::fs::create_dir_all(parent)?;
65        }
66
67        let contents = toml::to_string_pretty(self)?;
68        std::fs::write(&taps_file, contents)?;
69        Ok(())
70    }
71
72    /// Add a tap
73    pub fn add(&mut self, tap: Tap) {
74        self.taps.insert(tap.name.clone(), tap);
75    }
76
77    /// Remove a tap
78    pub fn remove(&mut self, name: &str) {
79        self.taps.remove(name);
80    }
81
82    /// Get a tap by name
83    pub fn get(&self, name: &str) -> Option<&Tap> {
84        self.taps.get(name)
85    }
86
87    /// List all taps
88    pub fn list(&self) -> Vec<&Tap> {
89        let mut taps: Vec<_> = self.taps.values().collect();
90        taps.sort_by(|a, b| a.name.cmp(&b.name));
91        taps
92    }
93
94    /// Get all tap URLs
95    pub fn urls(&self) -> Vec<(&str, &str)> {
96        self.taps
97            .iter()
98            .map(|(name, tap)| (name.as_str(), tap.url.as_str()))
99            .collect()
100    }
101
102    /// Check if a tap exists
103    pub fn contains(&self, name: &str) -> bool {
104        self.taps.contains_key(name)
105    }
106
107    /// Pin a tap (prevent updates)
108    pub fn pin(&mut self, name: &str) -> bool {
109        if let Some(tap) = self.taps.get_mut(name) {
110            tap.pinned = true;
111            true
112        } else {
113            false
114        }
115    }
116
117    /// Unpin a tap
118    pub fn unpin(&mut self, name: &str) -> bool {
119        if let Some(tap) = self.taps.get_mut(name) {
120            tap.pinned = false;
121            true
122        } else {
123            false
124        }
125    }
126}