diff options
author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2025-04-04 11:48:44 +0200 |
---|---|---|
committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2025-04-04 11:48:44 +0200 |
commit | 135d09bfb305d54cac1ba1fb9861d5b9309a7b3a (patch) | |
tree | 459109a40320530993ae560f55a730a72df31416 /pkgs/by-name/ts/tskm/src/interface/input/mod.rs | |
parent | refactor(modules/legacy/firefox): Move to by-name (diff) | |
download | nixos-config-135d09bfb305d54cac1ba1fb9861d5b9309a7b3a.zip |
feat(pkgs/neorg): Rewrite in rust
This improves upon neorg by integrating it better into the system context.
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.rs | 257 |
1 files changed, 257 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..9ece7a3a --- /dev/null +++ b/pkgs/by-name/ts/tskm/src/interface/input/mod.rs @@ -0,0 +1,257 @@ +use std::{ + collections::HashSet, + fmt::Display, + fs::{self, read_to_string, File}, + 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 NoWhitespaceString(String); + +impl NoWhitespaceString { + /// # Panics + /// If the input contains whitespace. + #[must_use] + pub fn new(input: String) -> Self { + if input.contains(' ') { + panic!("Your input '{input}' contains whitespace. I did not expect that.") + } else { + Self(input) + } + } +} + +impl Display for NoWhitespaceString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl NoWhitespaceString { + #[must_use] + pub fn as_str(&self) -> &str { + &self.0 + } +} + +#[derive(Debug, Clone)] +pub struct Input { + url: Url, + tags: HashSet<NoWhitespaceString>, +} + +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| { + if let Some(tag) = tag.strip_prefix('+') { + Ok(NoWhitespaceString::new(tag.to_owned())) + } else { + bail!("Your tag '{tag}' does not start with the required '+'"); + } + }) + .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() + .fold(String::new(), |mut acc, tag| { + acc.push('+'); + acc.push_str(tag.as_str()); + acc.push(' '); + acc + }) + .trim() + ) + } + } +} + +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.to_string()); + 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 url_content = { + if url_path.exists() { + read_to_string(&url_path)? + } else { + String::new() + } + }; + + let mut file = File::create(&url_path) + .with_context(|| format!("Failed to open file: '{}'", url_path.display()))?; + writeln!(file, "{url_content}{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(()) + } + + /// Commit your changes + fn git_commit(message: &str) -> Result<()> { + 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(()) + } + + /// Get all previously [`Self::commit`]ed 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() + .to_str() + .expect("All of these should be URLs and thus valid strings"); + assert!(url_value_file.ends_with("/url_value")); + + let url = { + let base = url_value_file + .strip_prefix(&format!("{}/", Self::base_path().display())) + .expect("This will exist"); + + let (proto, path) = base.split_once(':').expect("This will countain a :"); + + let path = path.strip_suffix("/url_value").expect("Will exist"); + + Url::from_str(&format!("{proto}:/{path}")) + .expect("This was a URL, it should still be one") + }; + let tags = { + let url_values = read_to_string(PathBuf::from(url_value_file))?; + url_values + .lines() + .map(|line| { + let input = Self::from_str(line)?; + Ok::<_, anyhow::Error>(input.tags) + }) + .collect::<Result<Vec<HashSet<NoWhitespaceString>>, _>>()? + .into_iter() + .flatten() + .collect() + }; + + output.push(Self { url, tags }); + } + + Ok(output) + } +} |