about summary refs log tree commit diff stats
path: root/pkgs/by-name/ts/tskm/src/interface/input/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/by-name/ts/tskm/src/interface/input/mod.rs')
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/input/mod.rs260
1 files changed, 260 insertions, 0 deletions
diff --git a/pkgs/by-name/ts/tskm/src/interface/input/mod.rs b/pkgs/by-name/ts/tskm/src/interface/input/mod.rs
new file mode 100644
index 00000000..1d1d67f4
--- /dev/null
+++ b/pkgs/by-name/ts/tskm/src/interface/input/mod.rs
@@ -0,0 +1,260 @@
+// nixos-config - My current NixOS configuration
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of my nixos-config.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::{
+    collections::{HashMap, HashSet},
+    fmt::Display,
+    fs,
+    io::Write,
+    path::PathBuf,
+    process::Command,
+    str::FromStr,
+};
+
+use anyhow::{bail, Context, Result};
+use url::Url;
+use walkdir::WalkDir;
+
+pub mod handle;
+pub use handle::handle;
+
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Tag(String);
+
+impl Tag {
+    pub fn new(input: &str) -> Result<Self> {
+        Self::from_str(input)
+    }
+}
+
+impl FromStr for Tag {
+    type Err = anyhow::Error;
+
+    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
+        if let Some(tag) = s.strip_prefix('+') {
+            if tag.contains(' ') {
+                bail!("Your tag '{s}' should not whitespace.")
+            }
+
+            Ok(Self(tag.to_owned()))
+        } else {
+            bail!("Your tag '{s}' does not start with the required '+'");
+        }
+    }
+}
+
+impl Display for Tag {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "+{}", self.0)
+    }
+}
+
+impl Tag {
+    #[must_use]
+    pub fn as_str(&self) -> &str {
+        &self.0
+    }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Input {
+    url: Url,
+    tags: HashSet<Tag>,
+}
+
+impl FromStr for Input {
+    type Err = anyhow::Error;
+
+    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
+        if s.contains(' ') {
+            let (url, tags) = s.split_once(' ').expect("Should work");
+            Ok(Self {
+                url: Url::from_str(url)?,
+                tags: {
+                    tags.trim()
+                        .split(' ')
+                        .map(Tag::new)
+                        .collect::<Result<_, _>>()?
+                },
+            })
+        } else {
+            Ok(Self {
+                url: Url::from_str(s)?,
+                tags: HashSet::new(),
+            })
+        }
+    }
+}
+
+impl Display for Input {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        if self.tags.is_empty() {
+            self.url.fmt(f)
+        } else {
+            write!(
+                f,
+                "{} {}",
+                self.url,
+                self.tags
+                    .iter()
+                    .map(ToString::to_string)
+                    .collect::<Vec<_>>()
+                    .join(" ")
+            )
+        }
+    }
+}
+
+impl Input {
+    fn base_path() -> PathBuf {
+        dirs::data_local_dir()
+            .expect("This should be set")
+            .join("tskm/inputs")
+    }
+
+    fn url_path(url: &Url) -> Result<PathBuf> {
+        let base_path = Self::base_path();
+
+        let url_path = base_path
+            .join(url.scheme())
+            .join(url.host_str().unwrap_or("<No Host>"))
+            .join(url.path().trim_matches('/'));
+        fs::create_dir_all(&url_path)
+            .with_context(|| format!("Failed to open file: '{}'", url_path.display()))?;
+
+        Ok(url_path.join("url_value"))
+    }
+
+    #[must_use]
+    pub fn url(&self) -> &Url {
+        &self.url
+    }
+
+    /// Commit this constructed [`Input`] to storage.
+    ///
+    /// # Errors
+    /// If IO operations fail.
+    pub fn commit(&self) -> Result<()> {
+        let url_path = Self::url_path(&self.url)?;
+
+        let mut file = fs::OpenOptions::new()
+            .create(true)
+            .append(true)
+            .open(&url_path)
+            .with_context(|| format!("Failed to open file: '{}'", url_path.display()))?;
+        writeln!(file, "{self}")?;
+
+        Self::git_commit(&format!("Add new url: '{self}'"))?;
+
+        Ok(())
+    }
+
+    /// Remove this constructed [`Input`] to storage.
+    ///
+    /// Beware that this does not take tags into account.
+    ///
+    /// # Errors
+    /// If IO operations fail.
+    pub fn remove(&self) -> Result<()> {
+        let url_path = Self::url_path(&self.url)?;
+
+        fs::remove_file(&url_path)
+            .with_context(|| format!("Failed to remove file: '{}'", url_path.display()))?;
+
+        let mut url_path = url_path.as_path();
+        while let Some(parent) = url_path.parent() {
+            if fs::read_dir(parent)?.count() == 0 {
+                fs::remove_dir(parent)?;
+            }
+            url_path = parent;
+        }
+
+        Self::git_commit(&format!("Remove url: '{self}'"))?;
+        Ok(())
+    }
+
+    /// Get all previously [committed][`Self::commit`] inputs.
+    ///
+    /// # Errors
+    /// When IO handling fails.
+    ///
+    /// # Panics
+    /// If internal assertions fail.
+    pub fn all() -> Result<Vec<Self>> {
+        let mut output = vec![];
+        for entry in WalkDir::new(Self::base_path())
+            .min_depth(1)
+            .into_iter()
+            .filter_entry(|e| {
+                let s = e.file_name().to_str();
+                s != Some(".git")
+            })
+        {
+            let entry = entry?;
+
+            if !entry.file_type().is_file() {
+                continue;
+            }
+
+            let url_value_file = entry.path();
+            assert!(url_value_file.ends_with("url_value"));
+
+            let url_values = fs::read_to_string(PathBuf::from(url_value_file))?;
+
+            let mut inputs: HashMap<Url, Self> = HashMap::new();
+            for input in url_values
+                .lines()
+                .map(Self::from_str)
+                .collect::<Result<Vec<Self>, _>>()?
+            {
+                if let Some(found) = inputs.get_mut(&input.url) {
+                    found.tags = found.tags.union(&input.tags).cloned().collect();
+                } else {
+                    assert_eq!(inputs.insert(input.url.clone(), input), None);
+                }
+            }
+
+            output.extend(inputs.drain().map(|(_, value)| value));
+        }
+
+        Ok(output)
+    }
+
+    /// Commit your changes
+    fn git_commit(message: &str) -> Result<()> {
+        if !Self::base_path().join(".git").exists() {
+            let status = Command::new("git")
+                .args(["init"])
+                .current_dir(Self::base_path())
+                .status()?;
+            if !status.success() {
+                bail!("Git init failed!");
+            }
+        }
+
+        let status = Command::new("git")
+            .args(["add", "."])
+            .current_dir(Self::base_path())
+            .status()?;
+        if !status.success() {
+            bail!("Git add . failed!");
+        }
+
+        let status = Command::new("git")
+            .args(["commit", "--message", message, "--no-gpg-sign"])
+            .current_dir(Self::base_path())
+            .status()?;
+        if !status.success() {
+            bail!("Git commit failed!");
+        }
+
+        Ok(())
+    }
+}