about summary refs log tree commit diff stats
path: root/pkgs/by-name/ts/tskm/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--pkgs/by-name/ts/tskm/src/cli.rs153
-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
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/mod.rs4
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs92
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/neorg/mod.rs18
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/open/handle.rs222
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/open/mod.rs106
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/project/handle.rs87
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/project/mod.rs73
-rw-r--r--pkgs/by-name/ts/tskm/src/main.rs67
-rw-r--r--pkgs/by-name/ts/tskm/src/rofi/mod.rs37
-rw-r--r--pkgs/by-name/ts/tskm/src/state.rs45
-rw-r--r--pkgs/by-name/ts/tskm/src/task/mod.rs342
14 files changed, 1615 insertions, 0 deletions
diff --git a/pkgs/by-name/ts/tskm/src/cli.rs b/pkgs/by-name/ts/tskm/src/cli.rs
new file mode 100644
index 00000000..1c72b3c2
--- /dev/null
+++ b/pkgs/by-name/ts/tskm/src/cli.rs
@@ -0,0 +1,153 @@
+use std::path::PathBuf;
+
+use anyhow::{bail, Result};
+use clap::{ArgAction, Parser, Subcommand};
+use url::Url;
+
+use crate::{
+    interface::{input::Input, project::ProjectName},
+    state::State,
+    task,
+};
+
+#[derive(Parser, Debug)]
+#[command(author, version, about, long_about, verbatim_doc_comment)]
+/// This is the core interface to the system-integrated task management
+///
+/// `tskm` effectively combines multiple applications together:
+/// - `taskwarrior` projects are raised connected to `firefox` profiles, making it possible to “open”
+///   a project.
+/// - Every `taskwarrior` project has a determined `neorg` path, so that extra information for a
+///   `project` can be stored in this `norg` file.
+/// - `tskm` can track inputs for you. These are URLs with optional tags which you can that
+///   “review” to open tasks based on them.
+pub struct CliArgs {
+    #[command(subcommand)]
+    pub command: Command,
+
+    /// Increase message verbosity
+    #[arg(long="verbose", short = 'v', action = ArgAction::Count, default_value_t = 2)]
+    pub verbosity: u8,
+
+    /// Silence all output
+    #[arg(long, short = 'q')]
+    pub quiet: bool,
+}
+
+#[derive(Subcommand, Debug)]
+pub enum Command {
+    /// Interact with projects.
+    Projects {
+        #[command(subcommand)]
+        command: ProjectCommand,
+    },
+
+    /// Manage the input queue.
+    Inputs {
+        #[command(subcommand)]
+        command: InputCommand,
+    },
+
+    /// Access the associated `neorg` workspace for the project/task.
+    Neorg {
+        #[command(subcommand)]
+        command: NeorgCommand,
+    },
+
+    /// Interface with the Firefox profile of each project.
+    Open {
+        #[command(subcommand)]
+        command: OpenCommand,
+    },
+}
+
+#[derive(Subcommand, Debug)]
+pub enum ProjectCommand {
+    /// Lists all available projects.
+    List,
+
+    /// Allows you to quickly add projects.
+    Add {
+        /// The name of the new project.
+        #[arg(value_parser = ProjectName::try_from_project)]
+        new_project_name: ProjectName,
+    },
+}
+
+#[derive(Subcommand, Debug, Clone, Copy)]
+pub enum NeorgCommand {
+    /// Open the `neorg` project associated with id of the task.
+    Task {
+        /// The working set id of the task
+        #[arg(value_parser = task_from_working_set_id)]
+        id: task::Task,
+    },
+}
+
+fn task_from_working_set_id(id: &str) -> Result<task::Task> {
+    let id: usize = id.parse()?;
+    let mut state = State::new_ro()?;
+
+    let Some(task) = task::Task::from_working_set(id, &mut state)? else {
+        bail!("Working set id '{id}' is not valid!")
+    };
+    Ok(task)
+}
+
+#[derive(Subcommand, Debug)]
+pub enum OpenCommand {
+    /// Open each project's Firefox profile consecutively, that was opened since the last review.
+    ///
+    /// This allows you to remove stale opened tabs and to commit open tabs to the `inputs`.
+    Review,
+
+    /// Opens Firefox with either the supplied project or the currently active project profile.
+    Project {
+        /// The project to open.
+        #[arg(value_parser = task::Project::from_project_string)]
+        project: task::Project,
+
+        /// The URL to open.
+        url: Option<Url>,
+    },
+
+    /// Open a selected project in it's Firefox profile.
+    ///
+    /// This will use rofi's dmenu mode to select one project from the list of all registered
+    /// projects.
+    Select {
+        /// The URL to open.
+        url: Option<Url>,
+    },
+
+    /// List all open tabs in the project.
+    ListTabs {
+        /// The project to open.
+        #[arg(value_parser = task::Project::from_project_string)]
+        project: Option<task::Project>,
+    },
+}
+
+#[derive(Subcommand, Debug)]
+pub enum InputCommand {
+    /// Add URLs as inputs to be categorized.
+    Add { inputs: Vec<Input> },
+    /// Remove URLs
+    Remove { inputs: Vec<Input> },
+
+    /// Add all URLs in the file as inputs to be categorized.
+    ///
+    /// This expects each line to contain one URL.
+    File { file: PathBuf },
+
+    /// Like 'review', but for the inputs that have previously been added.
+    /// It takes a project in which to open the URLs.
+    Review {
+        /// Opens all the URLs in this project.
+        #[arg(value_parser = task::Project::from_project_string)]
+        project: task::Project,
+    },
+
+    /// List all the previously added inputs.
+    List,
+}
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)
+    }
+}
diff --git a/pkgs/by-name/ts/tskm/src/interface/mod.rs b/pkgs/by-name/ts/tskm/src/interface/mod.rs
new file mode 100644
index 00000000..1a0d934c
--- /dev/null
+++ b/pkgs/by-name/ts/tskm/src/interface/mod.rs
@@ -0,0 +1,4 @@
+pub mod input;
+pub mod neorg;
+pub mod open;
+pub mod project;
diff --git a/pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs b/pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs
new file mode 100644
index 00000000..577de02c
--- /dev/null
+++ b/pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs
@@ -0,0 +1,92 @@
+use std::{
+    env,
+    fs::{self, read_to_string, File, OpenOptions},
+    io::Write,
+    process::Command,
+};
+
+use anyhow::{bail, Context, Result};
+
+use crate::{cli::NeorgCommand, state::State};
+
+pub fn handle(command: NeorgCommand, state: &mut State) -> Result<()> {
+    match command {
+        NeorgCommand::Task { id } => {
+            let project = id.project(state)?;
+            let path = dirs::data_local_dir()
+                .expect("This should exists")
+                .join("tskm/notes")
+                .join(project.get_neorg_path()?);
+
+            fs::create_dir_all(path.parent().expect("This should exist"))?;
+
+            {
+                let contents = if path.exists() {
+                    read_to_string(&path)
+                        .with_context(|| format!("Failed to read file: '{}'", path.display()))?
+                } else {
+                    File::create(&path)
+                        .with_context(|| format!("Failed to create file: '{}'", path.display()))?;
+                    String::new()
+                };
+
+                if !contents.contains(format!("% {}", id.uuid()).as_str()) {
+                    let mut options = OpenOptions::new();
+                    options.append(true).create(false);
+
+                    let mut file = options.open(&path)?;
+                    file.write_all(format!("* TITLE (% {})", id.uuid()).as_bytes())
+                        .with_context(|| {
+                            format!("Failed to write task uuid to file: '{}'", path.display())
+                        })?;
+                    file.flush()
+                        .with_context(|| format!("Failed to flush file: '{}'", path.display()))?;
+                }
+            }
+
+            let editor = env::var("EDITOR").unwrap_or("nvim".to_owned());
+            let status = Command::new(editor)
+                .args([
+                    path.to_str().expect("Should be a utf-8 str"),
+                    "-c",
+                    format!("/% {}", id.uuid()).as_str(),
+                ])
+                .status()?;
+            if !status.success() {
+                bail!("$EDITOR fail with error code: {status}");
+            }
+
+            {
+                let status = Command::new("git")
+                    .args(["add", "."])
+                    .current_dir(path.parent().expect("Will exist"))
+                    .status()?;
+                if !status.success() {
+                    bail!("Git add . failed!");
+                }
+
+                let status = Command::new("git")
+                    .args([
+                        "commit",
+                        "--message",
+                        format!(
+                            "chore({}): Update",
+                            path.parent().expect("Should have a parent").display()
+                        )
+                        .as_str(),
+                        "--no-gpg-sign",
+                    ])
+                    .current_dir(path.parent().expect("Will exist"))
+                    .status()?;
+                if !status.success() {
+                    bail!("Git commit failed!");
+                }
+            }
+
+            {
+                id.mark_neorg_data(state)?;
+            }
+        }
+    }
+    Ok(())
+}
diff --git a/pkgs/by-name/ts/tskm/src/interface/neorg/mod.rs b/pkgs/by-name/ts/tskm/src/interface/neorg/mod.rs
new file mode 100644
index 00000000..dc5cdf19
--- /dev/null
+++ b/pkgs/by-name/ts/tskm/src/interface/neorg/mod.rs
@@ -0,0 +1,18 @@
+use std::path::PathBuf;
+
+use anyhow::Result;
+
+use crate::task::{run_task, Project};
+
+pub mod handle;
+pub use handle::handle;
+
+impl Project {
+    pub(super) fn get_neorg_path(&self) -> Result<PathBuf> {
+        let project_path = run_task(&[
+            "_get",
+            format!("rc.context.{}.rc.neorg_path", self.to_context_display()).as_str(),
+        ])?;
+        Ok(PathBuf::from(project_path.as_str()))
+    }
+}
diff --git a/pkgs/by-name/ts/tskm/src/interface/open/handle.rs b/pkgs/by-name/ts/tskm/src/interface/open/handle.rs
new file mode 100644
index 00000000..15c7ac4d
--- /dev/null
+++ b/pkgs/by-name/ts/tskm/src/interface/open/handle.rs
@@ -0,0 +1,222 @@
+use std::{
+    fs,
+    net::{IpAddr, Ipv4Addr},
+    path::PathBuf,
+    process,
+};
+
+use anyhow::{bail, Context, Result};
+use log::{error, info};
+use url::Url;
+
+use crate::{cli::OpenCommand, rofi, state::State, task};
+
+pub fn handle(command: OpenCommand, state: &mut State) -> Result<()> {
+    match command {
+        OpenCommand::Review => {
+            for project in task::Project::all().context("Failed to get all project files")? {
+                if project.is_touched() {
+                    info!("Reviewing project: '{}'", project.to_project_display());
+                    open_in_browser(project, state, None).with_context(|| {
+                        format!(
+                            "Failed to open project ('{}') in Firefox",
+                            project.to_project_display()
+                        )
+                    })?;
+                    project.untouch().with_context(|| {
+                        format!(
+                            "Failed to untouch project ('{}')",
+                            project.to_project_display()
+                        )
+                    })?;
+                }
+            }
+        }
+        OpenCommand::Project { project, url } => {
+            project.touch().context("Failed to touch project")?;
+            open_in_browser(&project, state, url).with_context(|| {
+                format!("Failed to open project: {}", project.to_project_display())
+            })?;
+        }
+        OpenCommand::Select { url } => {
+            let selected_project: task::Project = task::Project::from_project_string(
+                &rofi::select(
+                    task::Project::all()
+                        .context("Failed to get all registered projects")?
+                        .iter()
+                        .map(task::Project::to_project_display)
+                        .collect::<Vec<_>>()
+                        .as_slice(),
+                )
+                .context("Failed to get selected project")?,
+            )
+            .expect("This should work, as we send only projects in");
+
+            selected_project
+                .touch()
+                .context("Failed to touch project")?;
+
+            open_in_browser(&selected_project, state, url).context("Failed to open project")?;
+        }
+        OpenCommand::ListTabs { project } => {
+            let project = if let Some(p) = project {
+                p
+            } else if let Some(p) =
+                task::Project::get_current().context("Failed to get currently focused project")?
+            {
+                p
+            } else {
+                bail!("You need to either supply a project or have a project active!");
+            };
+
+            let session_store = project.get_sessionstore().with_context(|| {
+                format!(
+                    "Failed to get session store for project: '{}'",
+                    project.to_project_display()
+                )
+            })?;
+
+            let selected = session_store
+                .windows
+                .iter()
+                .map(|w| w.selected)
+                .collect::<Vec<_>>();
+
+            let tabs = session_store
+                .windows
+                .iter()
+                .flat_map(|window| window.tabs.iter())
+                .map(|tab| tab.entries.get(tab.index - 1).expect("This should be Some"))
+                .collect::<Vec<_>>();
+
+            for (index, entry) in tabs.iter().enumerate() {
+                let index = index + 1;
+                let is_selected = {
+                    if selected.contains(&index) {
+                        "🔻 "
+                    } else {
+                        "   "
+                    }
+                };
+                println!("{}{}", is_selected, entry.url);
+            }
+        }
+    }
+    Ok(())
+}
+
+fn open_in_browser(
+    selected_project: &task::Project,
+    state: &mut State,
+    url: Option<Url>,
+) -> Result<()> {
+    let old_project: Option<task::Project> =
+        task::Project::get_current().context("Failed to get currently active project")?;
+    let old_task: Option<task::Task> =
+        task::Task::get_current(state).context("Failed to get currently active task")?;
+
+    selected_project.activate().with_context(|| {
+        format!(
+            "Failed to active project: '{}'",
+            selected_project.to_project_display()
+        )
+    })?;
+
+    let tracking_task = {
+        let all_tasks = selected_project.get_tasks(state).with_context(|| {
+            format!(
+                "Failed to get assoctiated tasks for project: '{}'",
+                selected_project.to_project_display()
+            )
+        })?;
+
+        let tracking_task = all_tasks.into_iter().find(|t| {
+            let maybe_desc = t.description(state);
+            if let Ok(desc) = maybe_desc {
+                desc == "tracking"
+            } else {
+                error!(
+                    "Getting task description returned error: {}",
+                    maybe_desc.expect_err("We already check for Ok")
+                );
+                false
+            }
+        });
+
+        if let Some(task) = tracking_task {
+            info!(
+                "Starting task {} -> tracking",
+                selected_project.to_project_display()
+            );
+            task.start(state)
+                .with_context(|| format!("Failed to start task {task}"))?;
+        }
+        tracking_task
+    };
+
+    let status = {
+        let mut args = vec!["-P".to_owned(), selected_project.to_project_display()];
+        if let Some(url) = url {
+            args.push(url.to_string());
+        } else {
+            let (ip, pid): (IpAddr, u32) = {
+                let link = fs::read_link(
+                    dirs::home_dir()
+                        .expect("Exists")
+                        .join(".mozilla/firefox")
+                        .join(selected_project.to_project_display())
+                        .join("lock"),
+                )?;
+                let (ip, pid) = link
+                    .to_str()
+                    .expect("Should work")
+                    .split_once(':')
+                    .expect("The split works");
+
+                (
+                    ip.parse().expect("Should be a valid ip address"),
+                    pid.parse().expect("Should be a valid pid"),
+                )
+            };
+
+            assert_eq!(ip, Ipv4Addr::new(127, 0, 0, 1));
+            if PathBuf::from("/proc").join(pid.to_string()).exists() {
+                // Another Firefox instance has already been started for this project
+                // Add a buffer URL to force Firefox to open it in the already open instance
+                args.push("about:newtab".to_owned());
+            } else {
+                // This project does not yet have another Firefox instance
+                // We do not need to add anything to the arguments, Firefox will open a new
+                // instance.
+            }
+        };
+
+        process::Command::new("firefox")
+            .args(args)
+            .status()
+            .context("Failed to start firefox")?
+    };
+
+    if !status.success() {
+        error!("Firefox run exited with error.");
+    }
+
+    if let Some(task) = tracking_task {
+        task.stop(state)
+            .with_context(|| format!("Failed to stop task {task}"))?;
+    }
+    if let Some(task) = old_task {
+        task.start(state)
+            .with_context(|| format!("Failed to start task {task}"))?;
+    }
+
+    if let Some(project) = old_project {
+        project.activate().with_context(|| {
+            format!("Failed to active project {}", project.to_project_display())
+        })?;
+    } else {
+        task::Project::clear().context("Failed to clear currently focused project")?;
+    }
+
+    Ok(())
+}
diff --git a/pkgs/by-name/ts/tskm/src/interface/open/mod.rs b/pkgs/by-name/ts/tskm/src/interface/open/mod.rs
new file mode 100644
index 00000000..2dc75957
--- /dev/null
+++ b/pkgs/by-name/ts/tskm/src/interface/open/mod.rs
@@ -0,0 +1,106 @@
+use std::{collections::HashMap, fs::File, io};
+
+use anyhow::{Context, Result};
+use lz4_flex::decompress_size_prepended;
+use serde::Deserialize;
+use serde_json::Value;
+use url::Url;
+
+use crate::task::Project;
+
+pub mod handle;
+pub use handle::handle;
+
+impl Project {
+    pub(super) fn get_sessionstore(&self) -> Result<SessionStore> {
+        let path = dirs::home_dir()
+            .expect("Will exist")
+            .join(".mozilla/firefox")
+            .join(self.to_project_display())
+            .join("sessionstore-backups/recovery.jsonlz4");
+        let file = decompress_mozlz4(
+            File::open(&path)
+                .with_context(|| format!("Failed to open path '{}'", path.display()))?,
+        )
+        .with_context(|| format!("Failed to decompress file as mozlzh '{}'", path.display()))?;
+
+        let contents: SessionStore = serde_json::from_str(&file).with_context(|| {
+            format!(
+                "Failed to deserialize file ('{}') as session store.",
+                path.display()
+            )
+        })?;
+        Ok(contents)
+    }
+}
+
+fn decompress_mozlz4<P: io::Read>(mut file: P) -> Result<String> {
+    const MOZLZ4_MAGIC_NUMBER: &[u8] = b"mozLz40\0";
+
+    let mut buf = [0u8; 8];
+    file.read_exact(&mut buf)
+        .context("Failed to read the mozlz40 header.")?;
+
+    assert_eq!(buf, MOZLZ4_MAGIC_NUMBER);
+
+    let mut buf = vec![];
+    file.read_to_end(&mut buf).context("Failed to read file")?;
+
+    let uncompressed = decompress_size_prepended(&buf).context("Failed to decompress file")?;
+
+    Ok(String::from_utf8(uncompressed).expect("This should be valid json and thus utf8"))
+}
+
+#[derive(Deserialize, Debug)]
+pub struct SessionStore {
+    pub windows: Vec<Window>,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct Window {
+    pub tabs: Vec<Tab>,
+    pub selected: usize,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct Tab {
+    pub entries: Vec<TabEntry>,
+    #[serde(rename = "lastAccessed")]
+    pub last_accessed: u64,
+    pub hidden: bool,
+    #[serde(rename = "searchMode")]
+    pub search_mode: Option<Value>,
+    #[serde(rename = "userContextId")]
+    pub user_context_id: u32,
+    pub attributes: TabAttributes,
+    #[serde(rename = "extData")]
+    pub ext_data: Option<HashMap<String, Value>>,
+    pub index: usize,
+    #[serde(rename = "requestedIndex")]
+    pub requested_index: Option<u32>,
+    pub image: Option<Url>,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct TabEntry {
+    pub url: Url,
+    pub title: String,
+    #[serde(rename = "cacheKey")]
+    pub cache_key: u32,
+    #[serde(rename = "ID")]
+    pub id: u32,
+    #[serde(rename = "docshellUUID")]
+    pub docshell_uuid: Value,
+    #[serde(rename = "resultPrincipalURI")]
+    pub result_principal_uri: Option<Url>,
+    #[serde(rename = "hasUserInteraction")]
+    pub has_user_interaction: bool,
+    #[serde(rename = "triggeringPrincipal_base64")]
+    pub triggering_principal_base64: Value,
+    #[serde(rename = "docIdentifier")]
+    pub doc_identifier: u32,
+    pub persist: bool,
+}
+
+#[derive(Deserialize, Debug, Clone, Copy)]
+pub struct TabAttributes {}
diff --git a/pkgs/by-name/ts/tskm/src/interface/project/handle.rs b/pkgs/by-name/ts/tskm/src/interface/project/handle.rs
new file mode 100644
index 00000000..2b01f5d1
--- /dev/null
+++ b/pkgs/by-name/ts/tskm/src/interface/project/handle.rs
@@ -0,0 +1,87 @@
+use std::{env, fs::File, io::Write};
+
+use anyhow::{anyhow, Context, Result};
+use log::trace;
+
+use crate::{cli::ProjectCommand, task};
+
+use super::{ProjectDefinition, ProjectList, SortAlphabetically};
+
+/// # Panics
+/// If internal expectations fail.
+///
+/// # Errors
+/// If IO operations fail.
+pub fn handle(command: ProjectCommand) -> Result<()> {
+    match command {
+        ProjectCommand::List => {
+            for project in task::Project::all()? {
+                println!("{}", project.to_project_display());
+            }
+        }
+        ProjectCommand::Add {
+            mut new_project_name,
+        } => {
+            let project_file = env::var("TSKM_PROJECT_FILE")
+                .map_err(|err| anyhow!("The `TSKM_PROJECT_FILE` env var is unset: {err}"))?;
+
+            let mut projects_content: ProjectList =
+                serde_json::from_reader(File::open(&project_file).with_context(|| {
+                    format!("Failed to open project file ('{project_file:?}') for reading")
+                })?)?;
+
+            let first = new_project_name.project_segments.remove(0);
+            if let Some(mut definition) = projects_content.0.get_mut(&first) {
+                for segment in new_project_name.project_segments {
+                    if definition.subprojects.contains_key(&segment) {
+                        definition = definition
+                            .subprojects
+                            .get_mut(&segment)
+                            .expect("We checked");
+                    } else {
+                        let new_definition = ProjectDefinition::default();
+                        let output = definition
+                            .subprojects
+                            .insert(segment.clone(), new_definition);
+
+                        assert_eq!(output, None);
+
+                        definition = definition
+                            .subprojects
+                            .get_mut(&segment)
+                            .expect("Was just inserted");
+                    }
+                }
+            } else {
+                let mut orig_definition = ProjectDefinition::default();
+                let mut definition = &mut orig_definition;
+                for segment in new_project_name.project_segments {
+                    trace!("Adding segment: {segment}");
+
+                    let new_definition = ProjectDefinition::default();
+
+                    assert!(definition
+                        .subprojects
+                        .insert(segment.clone(), new_definition)
+                        .is_none());
+
+                    definition = definition
+                        .subprojects
+                        .get_mut(&segment)
+                        .expect("Was just inserted");
+                }
+                assert!(projects_content.0.insert(first, orig_definition).is_none());
+            };
+
+            let mut file = File::create(&project_file).with_context(|| {
+                format!("Failed to open project file ('{project_file:?}') for writing")
+            })?;
+            serde_json::to_writer_pretty(
+                &file,
+                &SortAlphabetically::<ProjectList>(projects_content),
+            )?;
+            writeln!(file)?;
+        }
+    }
+    Ok(())
+}
diff --git a/pkgs/by-name/ts/tskm/src/interface/project/mod.rs b/pkgs/by-name/ts/tskm/src/interface/project/mod.rs
new file mode 100644
index 00000000..62069746
--- /dev/null
+++ b/pkgs/by-name/ts/tskm/src/interface/project/mod.rs
@@ -0,0 +1,73 @@
+use std::collections::HashMap;
+
+use anyhow::Result;
+use serde::{Deserialize, Serialize};
+
+pub mod handle;
+pub use handle::handle;
+
+#[derive(Deserialize, Serialize)]
+struct ProjectList(HashMap<String, ProjectDefinition>);
+
+#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
+struct ProjectDefinition {
+    #[serde(default)]
+    #[serde(skip_serializing_if = "is_default")]
+    name: String,
+
+    #[serde(default)]
+    #[serde(skip_serializing_if = "is_default")]
+    prefix: String,
+
+    #[serde(default)]
+    #[serde(skip_serializing_if = "is_default")]
+    subprojects: HashMap<String, ProjectDefinition>,
+}
+
+fn is_default<T: Default + PartialEq>(input: &T) -> bool {
+    input == &T::default()
+}
+
+#[derive(Debug, Clone)]
+pub struct ProjectName {
+    project_segments: Vec<String>,
+}
+
+impl ProjectName {
+    #[must_use]
+    pub fn segments(&self) -> &[String] {
+        &self.project_segments
+    }
+
+    /// # Errors
+    /// Never.
+    pub fn try_from_project(s: &str) -> Result<Self> {
+        Ok(Self::from_project(s))
+    }
+    pub fn from_project(s: &str) -> Self {
+        let me = Self {
+            project_segments: s.split('.').map(ToOwned::to_owned).collect(),
+        };
+        me
+    }
+    pub fn from_context(s: &str) -> Self {
+        let me = Self {
+            project_segments: s.split('_').map(ToOwned::to_owned).collect(),
+        };
+        me
+    }
+}
+
+// Source: https://stackoverflow.com/a/67792465
+fn sort_alphabetically<T: Serialize, S: serde::Serializer>(
+    value: &T,
+    serializer: S,
+) -> Result<S::Ok, S::Error> {
+    let value = serde_json::to_value(value).map_err(serde::ser::Error::custom)?;
+    value.serialize(serializer)
+}
+
+#[derive(Serialize)]
+pub(super) struct SortAlphabetically<T: Serialize>(
+    #[serde(serialize_with = "sort_alphabetically")] T,
+);
diff --git a/pkgs/by-name/ts/tskm/src/main.rs b/pkgs/by-name/ts/tskm/src/main.rs
new file mode 100644
index 00000000..f4416c6d
--- /dev/null
+++ b/pkgs/by-name/ts/tskm/src/main.rs
@@ -0,0 +1,67 @@
+use anyhow::Result;
+use clap::Parser;
+use state::State;
+
+use crate::interface::{input, neorg, open, project};
+
+pub mod cli;
+pub mod interface;
+pub mod rofi;
+pub mod state;
+pub mod task;
+
+use crate::cli::{CliArgs, Command};
+
+fn main() -> Result<(), anyhow::Error> {
+    // TODO: Support these completions for the respective types <2025-04-04>
+    //
+    // ID_GENERATION_FUNCTION
+    // ```sh
+    //  context="$(task _get rc.context)"
+    //  if [ "$context" ]; then
+    //      filter="project:$context"
+    //  else
+    //      filter="0-10000"
+    //  fi
+    //  tasks="$(task "$filter" _ids)"
+    //
+    //  if [ "$tasks" ]; then
+    //      echo "$tasks" | xargs task _zshids | awk -F: -v q="'" '{gsub(/'\''/, q "\\" q q ); print $1 ":" q $2 q}'
+    //  fi
+    // ```
+    //
+    // ARGUMENTS:
+    //     ID | *([0-9]) := [[%ID_GENERATION_FUNCTION]]
+    //                             The function displays all possible IDs of the eligible tasks.
+    //
+    //     WS := %ALL_WORKSPACES
+    //                             All possible workspaces.
+    //
+    //     P := %ALL_PROJECTS_PIPE
+    //                             The possible project.
+    //
+    //     F := [[fd . --max-depth 3]]
+    //                             A URL-Input file to use as source.
+    let args = CliArgs::parse();
+
+    stderrlog::new()
+        .module(module_path!())
+        .quiet(args.quiet)
+        .show_module_names(true)
+        .color(stderrlog::ColorChoice::Auto)
+        .verbosity(usize::from(args.verbosity))
+        .timestamp(stderrlog::Timestamp::Off)
+        .init()
+        .expect("Let's just hope that this does not panic");
+
+    let mut state = State::new_rw()?;
+
+    match args.command {
+        Command::Inputs { command } => input::handle(command)?,
+        Command::Neorg { command } => neorg::handle(command, &mut state)?,
+        Command::Open { command } => open::handle(command, &mut state)?,
+        Command::Projects { command } => project::handle(command)?,
+    }
+
+    Ok(())
+}
diff --git a/pkgs/by-name/ts/tskm/src/rofi/mod.rs b/pkgs/by-name/ts/tskm/src/rofi/mod.rs
new file mode 100644
index 00000000..a0591b7f
--- /dev/null
+++ b/pkgs/by-name/ts/tskm/src/rofi/mod.rs
@@ -0,0 +1,37 @@
+use std::{
+    io::Write,
+    process::{Command, Stdio},
+};
+
+use anyhow::{Context, Result};
+
+pub fn select(options: &[String]) -> Result<String> {
+    let mut child = Command::new("rofi")
+        .args(["-sep", "\n", "-dmenu"])
+        .stdin(Stdio::piped())
+        .stdout(Stdio::piped())
+        .spawn()
+        .context("Failed to spawn rofi")?;
+
+    let mut stdin = child
+        .stdin
+        .take()
+        .expect("We piped this, so should be available");
+
+    stdin
+        .write_all(options.join("\n").as_bytes())
+        .context("Failed to write to rofi's stdin")?;
+
+    let output = child
+        .wait_with_output()
+        .context("Failed to wait for rofi's output")?;
+
+    let selected = String::from_utf8(output.stdout.clone()).with_context(|| {
+        format!(
+            "Failed to decode '{}' as utf8",
+            String::from_utf8_lossy(&output.stdout)
+        )
+    })?;
+
+    Ok(selected.trim_end().to_owned())
+}
diff --git a/pkgs/by-name/ts/tskm/src/state.rs b/pkgs/by-name/ts/tskm/src/state.rs
new file mode 100644
index 00000000..175a7f03
--- /dev/null
+++ b/pkgs/by-name/ts/tskm/src/state.rs
@@ -0,0 +1,45 @@
+use std::path::PathBuf;
+
+use anyhow::Result;
+use taskchampion::{storage::AccessMode, Replica, StorageConfig};
+
+pub struct State {
+    replica: Replica,
+}
+
+impl std::fmt::Debug for State {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "State")
+    }
+}
+
+impl State {
+    fn taskdb_dir() -> PathBuf {
+        dirs::data_local_dir().expect("Should exist").join("task")
+    }
+
+    fn new(taskdb_dir: PathBuf, access_mode: AccessMode) -> Result<Self> {
+        let storage = StorageConfig::OnDisk {
+            taskdb_dir,
+            create_if_missing: false,
+            access_mode,
+        }
+        .into_storage()?;
+
+        let replica = Replica::new(storage);
+
+        Ok(Self { replica })
+    }
+
+    pub fn new_ro() -> Result<Self> {
+        Self::new(Self::taskdb_dir(), AccessMode::ReadOnly)
+    }
+    pub fn new_rw() -> Result<Self> {
+        Self::new(Self::taskdb_dir(), AccessMode::ReadWrite)
+    }
+
+    #[must_use]
+    pub fn replica(&mut self) -> &mut Replica {
+        &mut self.replica
+    }
+}
diff --git a/pkgs/by-name/ts/tskm/src/task/mod.rs b/pkgs/by-name/ts/tskm/src/task/mod.rs
new file mode 100644
index 00000000..03a12faa
--- /dev/null
+++ b/pkgs/by-name/ts/tskm/src/task/mod.rs
@@ -0,0 +1,342 @@
+use std::{
+    fmt::Display,
+    fs::{self, read_to_string, File},
+    path::PathBuf,
+    process::Command,
+    str::FromStr,
+    sync::OnceLock,
+};
+
+use anyhow::{bail, Context, Result};
+use log::{debug, info, trace};
+use taskchampion::Tag;
+
+use crate::{interface::project::ProjectName, state::State};
+
+/// The `taskwarrior` id of a task.
+#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Ord, Eq)]
+pub struct Task {
+    uuid: taskchampion::Uuid,
+}
+
+impl From<&taskchampion::Task> for Task {
+    fn from(value: &taskchampion::Task) -> Self {
+        Self {
+            uuid: value.get_uuid(),
+        }
+    }
+}
+impl From<&taskchampion::TaskData> for Task {
+    fn from(value: &taskchampion::TaskData) -> Self {
+        Self {
+            uuid: value.get_uuid(),
+        }
+    }
+}
+
+impl Task {
+    pub fn from_working_set(id: usize, state: &mut State) -> Result<Option<Self>> {
+        Ok(state
+            .replica()
+            .working_set()?
+            .by_index(id)
+            .map(|uuid| Self { uuid }))
+    }
+
+    pub fn get_current(state: &mut State) -> Result<Option<Self>> {
+        let tasks = state
+            .replica()
+            .pending_tasks()?
+            .into_iter()
+            .filter(taskchampion::Task::is_active)
+            .collect::<Vec<_>>();
+
+        assert!(
+            tasks.len() <= 1,
+            "We have ensured that only one task may be active, via a hook"
+        );
+        if let Some(active) = tasks.first() {
+            Ok(Some(Self::from(active)))
+        } else {
+            Ok(None)
+        }
+    }
+
+    #[must_use]
+    pub fn uuid(&self) -> &taskchampion::Uuid {
+        &self.uuid
+    }
+
+    fn as_task(&self, state: &mut State) -> Result<taskchampion::Task> {
+        Ok(state
+            .replica()
+            .get_task(self.uuid)?
+            .expect("We have the task from this replica, it should still be in it"))
+    }
+
+    /// Adds a tag to the task, to show the user that it has additional neorg data.
+    pub fn mark_neorg_data(&self, state: &mut State) -> Result<()> {
+        let mut ops = vec![];
+        self.as_task(state)?
+            .add_tag(&Tag::from_str("neorg_data").expect("Is valid"), &mut ops)?;
+        state.replica().commit_operations(ops)?;
+        Ok(())
+    }
+
+    /// Try to start this task.
+    /// It will stop previously active tasks.
+    pub fn start(&self, state: &mut State) -> Result<()> {
+        info!("Activating {self}");
+
+        if let Some(active) = Self::get_current(state)? {
+            active.stop(state)?;
+        }
+
+        let mut ops = vec![];
+        self.as_task(state)?.start(&mut ops)?;
+        state.replica().commit_operations(ops)?;
+        Ok(())
+    }
+
+    /// Stops this task.
+    pub fn stop(&self, state: &mut State) -> Result<()> {
+        info!("Stopping {self}");
+
+        let mut ops = vec![];
+        self.as_task(state)?.stop(&mut ops)?;
+        state.replica().commit_operations(ops)?;
+        Ok(())
+    }
+
+    pub fn description(&self, state: &mut State) -> Result<String> {
+        Ok(self.as_task(state)?.get_description().to_owned())
+    }
+
+    pub fn project(&self, state: &mut State) -> Result<Project> {
+        let output = {
+            let task = self.as_task(state)?;
+            let task_data = task.into_task_data();
+            task_data
+                .get("project")
+                .expect("Every task should have a project")
+                .to_owned()
+        };
+        let project = Project::from_project_string(output.as_str())
+            .expect("This comes from tw, it should be valid");
+        Ok(project)
+    }
+}
+
+impl Display for Task {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.uuid.fmt(f)
+    }
+}
+
+impl FromStr for Task {
+    type Err = anyhow::Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let uuid = taskchampion::Uuid::from_str(s)?;
+        Ok(Self { uuid })
+    }
+}
+
+/// A registered task Project
+#[derive(Debug, Clone, PartialEq)]
+pub struct Project {
+    /// The project name.
+    /// For example:
+    /// ```no_run
+    /// &["trinitrix", "testing", "infra"]
+    /// ```
+    name: Vec<String>,
+}
+
+static ALL_CACHE: OnceLock<Vec<Project>> = OnceLock::new();
+impl Project {
+    #[must_use]
+    pub fn to_project_display(&self) -> String {
+        self.name.join(".")
+    }
+    #[must_use]
+    pub fn to_context_display(&self) -> String {
+        self.name.join("_")
+    }
+
+    /// # Errors
+    /// - When the string does not encode a previously registered project.
+    /// - When the string does not adhere to the project syntax.
+    pub fn from_project_string(s: &str) -> Result<Self> {
+        Self::from_input(s, ProjectName::from_project)
+    }
+
+    /// # Errors
+    /// - When the string does not encode a previously registered project.
+    /// - When the string does not adhere to the context syntax.
+    pub fn from_context_string(s: &str) -> Result<Self> {
+        Self::from_input(s, ProjectName::from_context)
+    }
+
+    fn from_input<F>(s: &str, f: F) -> Result<Self>
+    where
+        F: Fn(&str) -> ProjectName,
+    {
+        if s.is_empty() {
+            bail!("Your project is empty")
+        }
+
+        let all = Self::all()?;
+        let me = Self::from_project_name_unchecked(&f(s));
+        if all.contains(&me) {
+            Ok(me)
+        } else {
+            bail!(
+                "Your project '{}' is not registered!",
+                me.to_project_display()
+            );
+        }
+    }
+    fn from_project_name_unchecked(pn: &ProjectName) -> Self {
+        Self {
+            name: pn.segments().to_owned(),
+        }
+    }
+
+    /// Return all known valid projects.
+    ///
+    /// # Errors
+    /// When file operations fail.
+    ///
+    /// # Panics
+    /// Only when internal assertions fail.
+    pub fn all<'a>() -> Result<&'a [Project]> {
+        // Inlined from `OnceLock::get_or_try_init`
+        {
+            let this = &ALL_CACHE;
+            let f = || {
+                let file = dirs::config_local_dir()
+                    .expect("Should be some")
+                    .join("tskm/projects.list");
+                let contents = read_to_string(&file)
+                    .with_context(|| format!("Failed to read file: '{}'", file.display()))?;
+
+                Ok::<_, anyhow::Error>(
+                    contents
+                        .lines()
+                        .map(|s| Self::from_project_name_unchecked(&ProjectName::from_project(s)))
+                        .collect::<Vec<_>>(),
+                )
+            };
+
+            // Fast path check
+            // NOTE: We need to perform an acquire on the state in this method
+            // in order to correctly synchronize `LazyLock::force`. This is
+            // currently done by calling `self.get()`, which in turn calls
+            // `self.is_initialized()`, which in turn performs the acquire.
+            if let Some(value) = this.get() {
+                return Ok(value);
+            }
+
+            this.set(f()?).expect(
+                "This should always be able to take our value, as we initialize only once.",
+            );
+
+            Ok(this.get().expect("This was initialized"))
+        }
+    }
+
+    fn touch_dir(&self) -> PathBuf {
+        let lock_dir = dirs::data_dir()
+            .expect("Should be found")
+            .join("tskm/review");
+        lock_dir.join(format!("{}.opened", self.to_project_display()))
+    }
+
+    /// Mark this project as having been interacted with.
+    ///
+    /// # Errors
+    /// When IO operations fail.
+    pub fn touch(&self) -> Result<()> {
+        let lock_file = self.touch_dir();
+
+        File::create(&lock_file)
+            .with_context(|| format!("Failed to create lock_file at: {}", lock_file.display()))?;
+
+        Ok(())
+    }
+    /// Returns [`true`] if it was previously [`Self::touch`]ed.
+    #[must_use]
+    pub fn is_touched(&self) -> bool {
+        let lock_file = self.touch_dir();
+        lock_file.exists()
+    }
+    /// Mark this project as having not been interacted with.
+    ///
+    /// # Errors
+    /// When IO operations fail.
+    pub fn untouch(&self) -> Result<()> {
+        let lock_file = self.touch_dir();
+
+        fs::remove_file(&lock_file)
+            .with_context(|| format!("Failed to create lock_file at: {}", lock_file.display()))?;
+
+        Ok(())
+    }
+
+    /// # Errors
+    /// When `task` execution fails.
+    pub fn get_tasks(&self, state: &mut State) -> Result<Vec<Task>> {
+        Ok(state
+            .replica()
+            .pending_task_data()?
+            .into_iter()
+            .filter(|t| t.get("project").expect("Is set") == self.to_project_display())
+            .map(|t| Task::from(&t))
+            .collect())
+    }
+
+    /// # Errors
+    /// When `task` execution fails.
+    pub fn activate(&self) -> Result<()> {
+        debug!("Setting project {}", self.to_context_display());
+
+        run_task(&["context", self.to_context_display().as_str()]).map(|_| ())
+    }
+    /// # Errors
+    /// When `task` execution fails.
+    pub fn clear() -> Result<()> {
+        debug!("Clearing active project");
+
+        run_task(&["context", "none"]).map(|_| ())
+    }
+
+    /// # Errors
+    /// When `task` execution fails.
+    pub fn get_current() -> Result<Option<Self>> {
+        let self_str = run_task(&["_get", "rc.context"])?;
+
+        if self_str.is_empty() {
+            Ok(None)
+        } else {
+            Self::from_context_string(&self_str).map(Some)
+        }
+    }
+}
+
+pub(crate) fn run_task(args: &[&str]) -> Result<String> {
+    debug!("Running task command: `task {}`", args.join(" "));
+
+    let output = Command::new("task")
+        .args(args)
+        .output()
+        .with_context(|| format!("Failed to run `task {}`", args.join(" ")))?;
+
+    let stdout = String::from_utf8(output.stdout).context("Failed to read task output as utf8")?;
+    let stderr = String::from_utf8(output.stderr).context("Failed to read task output as utf8")?;
+
+    trace!("Output (stdout): '{}'", stdout.trim());
+    trace!("Output (stderr): '{}'", stderr.trim());
+
+    Ok(stdout.trim().to_owned())
+}