aboutsummaryrefslogtreecommitdiffstats
path: root/pkgs/by-name/ts/tskm/src/interface/input
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-04-04 11:48:44 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-04-04 11:48:44 +0200
commit135d09bfb305d54cac1ba1fb9861d5b9309a7b3a (patch)
tree459109a40320530993ae560f55a730a72df31416 /pkgs/by-name/ts/tskm/src/interface/input
parentrefactor(modules/legacy/firefox): Move to by-name (diff)
downloadnixos-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')
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/input/handle.rs112
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/input/mod.rs257
2 files changed, 369 insertions, 0 deletions
diff --git a/pkgs/by-name/ts/tskm/src/interface/input/handle.rs b/pkgs/by-name/ts/tskm/src/interface/input/handle.rs
new file mode 100644
index 00000000..0ff0e56e
--- /dev/null
+++ b/pkgs/by-name/ts/tskm/src/interface/input/handle.rs
@@ -0,0 +1,112 @@
+use std::{
+ fs, process,
+ str::FromStr,
+ thread::{self, sleep},
+ time::Duration,
+};
+
+use anyhow::{Context, Result};
+use log::{error, info};
+
+use crate::cli::InputCommand;
+
+use super::Input;
+
+/// # Errors
+/// When command handling fails.
+///
+/// # Panics
+/// When internal assertions fail.
+pub fn handle(command: InputCommand) -> Result<()> {
+ match command {
+ InputCommand::Add { inputs } => {
+ for input in inputs {
+ input.commit().with_context(|| {
+ format!("Failed to add input ('{input}') to the input storage.")
+ })?;
+ }
+ }
+ InputCommand::Remove { inputs } => {
+ for input in inputs {
+ input.remove().with_context(|| {
+ format!("Failed to remove input ('{input}') from the input storage.")
+ })?;
+ }
+ }
+ InputCommand::File { file } => {
+ let file = fs::read_to_string(file)?;
+ for line in file.lines() {
+ let input = Input::from_str(line)?;
+ input.commit().with_context(|| {
+ format!("Failed to add input ('{input}') to the input storage.")
+ })?;
+ }
+ }
+ InputCommand::Review { project } => {
+ let project = project.to_project_display();
+
+ let local_project = project.clone();
+ let handle = thread::spawn(move || {
+ // We assume that the project is not yet open.
+ let mut firefox = process::Command::new("firefox")
+ .args(["-P", local_project.as_str(), "about:newtab"])
+ .spawn()?;
+
+ Ok::<_, anyhow::Error>(firefox.wait()?)
+ });
+ // Give Firefox some time to start.
+ info!("Waiting on firefox to start");
+ sleep(Duration::from_secs(4));
+
+ let project_str = project.as_str();
+ 'outer: for all in Input::all()?.chunks(100) {
+ info!("Starting review for the first hundred URLs.");
+
+ for input in all {
+ info!("-> '{input}'");
+ let status = process::Command::new("firefox")
+ .args(["-P", project_str, input.url().to_string().as_str()])
+ .status()?;
+
+ if status.success() {
+ input.remove()?;
+ } else {
+ error!("Adding `{input}` to Firefox failed!");
+ }
+ }
+
+ {
+ use std::io::{stdin, stdout, Write};
+
+ let mut s = String::new();
+ eprint!("Continue? (y/N) ");
+ stdout().flush()?;
+
+ stdin()
+ .read_line(&mut s)
+ .expect("Did not enter a correct string");
+
+ if let Some('\n') = s.chars().next_back() {
+ s.pop();
+ }
+ if let Some('\r') = s.chars().next_back() {
+ s.pop();
+ }
+
+ if s != "y" {
+ break 'outer;
+ }
+ }
+ }
+
+ info!("Waiting for firefox to stop");
+ handle.join().expect("Should be joinable")?;
+ }
+ InputCommand::List => {
+ for url in Input::all()? {
+ println!("{url}");
+ }
+ }
+ }
+ Ok(())
+}
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)
+ }
+}