about summary refs log tree commit diff stats
path: root/pkgs/by-name/ts
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--pkgs/by-name/ts/tskm/Cargo.lock122
-rw-r--r--pkgs/by-name/ts/tskm/Cargo.toml23
-rw-r--r--pkgs/by-name/ts/tskm/flake.lock6
-rw-r--r--pkgs/by-name/ts/tskm/package.nix2
-rw-r--r--pkgs/by-name/ts/tskm/src/browser/mod.rs172
-rw-r--r--pkgs/by-name/ts/tskm/src/cli.rs88
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/input/handle.rs107
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/input/mod.rs185
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs20
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/neorg/mod.rs2
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/open/handle.rs292
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/open/mod.rs212
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/project/handle.rs12
-rw-r--r--pkgs/by-name/ts/tskm/src/main.rs4
-rw-r--r--pkgs/by-name/ts/tskm/src/state.rs2
-rw-r--r--pkgs/by-name/ts/tskm/src/task/mod.rs9
16 files changed, 773 insertions, 485 deletions
diff --git a/pkgs/by-name/ts/tskm/Cargo.lock b/pkgs/by-name/ts/tskm/Cargo.lock
index 13742dcb..8233a371 100644
--- a/pkgs/by-name/ts/tskm/Cargo.lock
+++ b/pkgs/by-name/ts/tskm/Cargo.lock
@@ -46,9 +46,9 @@ dependencies = [
 
 [[package]]
 name = "anstream"
-version = "0.6.18"
+version = "0.6.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
+checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
 dependencies = [
  "anstyle",
  "anstyle-parse",
@@ -61,33 +61,33 @@ dependencies = [
 
 [[package]]
 name = "anstyle"
-version = "1.0.10"
+version = "1.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
+checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
 
 [[package]]
 name = "anstyle-parse"
-version = "0.2.6"
+version = "0.2.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
 dependencies = [
  "utf8parse",
 ]
 
 [[package]]
 name = "anstyle-query"
-version = "1.1.2"
+version = "1.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
+checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
 dependencies = [
  "windows-sys",
 ]
 
 [[package]]
 name = "anstyle-wincon"
-version = "3.0.8"
+version = "3.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa"
+checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
 dependencies = [
  "anstyle",
  "once_cell_polyfill",
@@ -101,6 +101,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
 
 [[package]]
+name = "arraydeque"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
+
+[[package]]
 name = "autocfg"
 version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -114,9 +120,9 @@ checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
 
 [[package]]
 name = "bumpalo"
-version = "3.17.0"
+version = "3.18.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
+checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee"
 
 [[package]]
 name = "byteorder"
@@ -126,9 +132,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
 
 [[package]]
 name = "cc"
-version = "1.2.24"
+version = "1.2.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7"
+checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac"
 dependencies = [
  "shlex",
 ]
@@ -178,9 +184,9 @@ dependencies = [
 
 [[package]]
 name = "clap_complete"
-version = "4.5.51"
+version = "4.5.52"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d2267df7f3c8e74e38268887ea5235d4dfadd39bfff2d56ab82d61776be355e"
+checksum = "1a554639e42d0c838336fc4fbedb9e2df3ad1fa4acda149f9126b4ccfcd7900f"
 dependencies = [
  "clap",
  "clap_lex",
@@ -208,9 +214,9 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
 
 [[package]]
 name = "colorchoice"
-version = "1.0.3"
+version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
 
 [[package]]
 name = "core-foundation-sys"
@@ -260,6 +266,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
 name = "fallible-iterator"
 version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -282,6 +297,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
 name = "form_urlencoded"
 version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -323,12 +344,30 @@ dependencies = [
 ]
 
 [[package]]
+name = "hashbrown"
+version = "0.15.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
 name = "hashlink"
 version = "0.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
 dependencies = [
- "hashbrown",
+ "hashbrown 0.14.5",
+]
+
+[[package]]
+name = "hashlink"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
+dependencies = [
+ "hashbrown 0.15.3",
 ]
 
 [[package]]
@@ -559,9 +598,12 @@ name = "lz4_flex"
 version = "0.11.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5"
-dependencies = [
- "twox-hash",
-]
+
+[[package]]
+name = "md5"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
 
 [[package]]
 name = "memchr"
@@ -670,7 +712,7 @@ dependencies = [
  "bitflags",
  "fallible-iterator",
  "fallible-streaming-iterator",
- "hashlink",
+ "hashlink 0.9.1",
  "libsqlite3-sys",
  "smallvec",
 ]
@@ -736,9 +778,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
 
 [[package]]
 name = "smallvec"
-version = "1.15.0"
+version = "1.15.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
 
 [[package]]
 name = "stable_deref_trait"
@@ -747,18 +789,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
 
 [[package]]
-name = "static_assertions"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
-
-[[package]]
 name = "stderrlog"
 version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "61c910772f992ab17d32d6760e167d2353f4130ed50e796752689556af07dc6b"
 dependencies = [
- "chrono",
  "is-terminal",
  "log",
  "termcolor",
@@ -891,22 +926,14 @@ dependencies = [
  "dirs",
  "log",
  "lz4_flex",
+ "md5",
  "serde",
  "serde_json",
  "stderrlog",
  "taskchampion",
  "url",
  "walkdir",
-]
-
-[[package]]
-name = "twox-hash"
-version = "1.6.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
-dependencies = [
- "cfg-if",
- "static_assertions",
+ "yaml-rust2",
 ]
 
 [[package]]
@@ -1225,6 +1252,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
 
 [[package]]
+name = "yaml-rust2"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18b783b2c2789414f8bb84ca3318fc9c2d7e7be1c22907d37839a58dedb369d3"
+dependencies = [
+ "arraydeque",
+ "encoding_rs",
+ "hashlink 0.10.0",
+]
+
+[[package]]
 name = "yoke"
 version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/pkgs/by-name/ts/tskm/Cargo.toml b/pkgs/by-name/ts/tskm/Cargo.toml
index c25e62c3..42b91ae5 100644
--- a/pkgs/by-name/ts/tskm/Cargo.toml
+++ b/pkgs/by-name/ts/tskm/Cargo.toml
@@ -16,18 +16,19 @@ edition = "2021"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
-anyhow = "1.0.98"
-clap = { version = "4.5.39", features = ["derive"] }
-clap_complete = { version = "4.5.51", features = ["unstable-dynamic"] }
-dirs = "6.0.0"
-log = "0.4.27"
-lz4_flex = "0.11.3"
-serde = { version = "1.0.219", features = ["derive"] }
-serde_json = "1.0.140"
-stderrlog = "0.6.0"
+anyhow = { version = "1.0.98", default-features = false }
+clap = { version = "4.5.39", features = [ "derive", "std", "color", "help", "usage", "error-context", "suggestions", ], default-features = false }
+clap_complete = { version = "4.5.52", features = ["unstable-dynamic"] }
+dirs = { version = "6.0.0", default-features = false }
+log = { version = "0.4.27", default-features = false }
+serde = { version = "1.0.219", features = ["derive"], default-features = false }
+serde_json = { version = "1.0.140", default-features = false }
+stderrlog = { version = "0.6.0", default-features = false }
 taskchampion = { version = "2.0.3", default-features = false }
-url = { version = "2.5.4", features = ["serde"] }
-walkdir = "2.5.0"
+url = { version = "2.5.4", features = ["serde"], default-features = false }
+walkdir = { version = "2.5.0", default-features = false }
+md5 = { version = "0.7.0", default-features = false }
+yaml-rust2 = "0.10.2"
 
 [profile.release]
 lto = true
diff --git a/pkgs/by-name/ts/tskm/flake.lock b/pkgs/by-name/ts/tskm/flake.lock
index 280e8a51..ea422f02 100644
--- a/pkgs/by-name/ts/tskm/flake.lock
+++ b/pkgs/by-name/ts/tskm/flake.lock
@@ -2,11 +2,11 @@
   "nodes": {
     "nixpkgs": {
       "locked": {
-        "lastModified": 1748406211,
-        "narHash": "sha256-B3BsCRbc+x/d0WiG1f+qfSLUy+oiIfih54kalWBi+/M=",
+        "lastModified": 1749174413,
+        "narHash": "sha256-urN9UMK5cd1dzhR+Lx0xHeTgBp2MatA5+6g9JaxjuQs=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "3d1f29646e4b57ed468d60f9d286cde23a8d1707",
+        "rev": "6ad174a6dc07c7742fc64005265addf87ad08615",
         "type": "github"
       },
       "original": {
diff --git a/pkgs/by-name/ts/tskm/package.nix b/pkgs/by-name/ts/tskm/package.nix
index d75afe6e..ad10865f 100644
--- a/pkgs/by-name/ts/tskm/package.nix
+++ b/pkgs/by-name/ts/tskm/package.nix
@@ -16,7 +16,6 @@
   taskwarrior3,
   git,
   rofi,
-  firefox,
   sqlite,
 }:
 rustPlatform.buildRustPackage (finalAttrs: {
@@ -32,7 +31,6 @@ rustPlatform.buildRustPackage (finalAttrs: {
     taskwarrior3
     git
     rofi
-    firefox
     sqlite
   ];
 
diff --git a/pkgs/by-name/ts/tskm/src/browser/mod.rs b/pkgs/by-name/ts/tskm/src/browser/mod.rs
new file mode 100644
index 00000000..8dd52663
--- /dev/null
+++ b/pkgs/by-name/ts/tskm/src/browser/mod.rs
@@ -0,0 +1,172 @@
+use std::{
+    env,
+    io::Write,
+    os::unix::net::UnixStream,
+    path::PathBuf,
+    process::{self, ExitStatus},
+};
+
+use anyhow::{Context, Result};
+use log::{error, info};
+use serde_json::json;
+use url::Url;
+
+use crate::{state::State, task};
+
+#[allow(clippy::too_many_lines)]
+pub 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 = {
+        // #!/bin/sh
+        // # initial idea: Florian Bruhin (The-Compiler)
+        // # author: Thore Bödecker (foxxx0)
+        //
+        // _url="$1"
+        // _qb_version='1.0.4'
+        // _proto_version=1
+        // _ipc_socket="${XDG_RUNTIME_DIR}/qutebrowser/ipc-$(printf '%s' "$USER" | md5sum | cut -d' ' -f1)"
+        // _qute_bin="/usr/bin/qutebrowser"
+        //
+        // printf '{"args": ["%s"], "target_arg": null, "version": "%s", "protocol_version": %d, "cwd": "%s"}\n' \
+        //        "${_url}" \
+        //        "${_qb_version}" \
+        //        "${_proto_version}" \
+        //        "${PWD}" | socat -lf /dev/null - UNIX-CONNECT:"${_ipc_socket}" || "$_qute_bin" "$@" &
+
+        let ipc_socket_path = PathBuf::from(
+            env::var("XDG_RUNTIME_DIR").context("Failed to access XDG_RUNTIME_DIR var")?,
+        )
+        .join("qutebrowser")
+        .join(selected_project.to_project_display())
+        .join(format!("ipc-{:x}", {
+            let user_name = env::var("USER").context("Failed to get USER var")?;
+            let base_dir = env::var("XDG_DATA_HOME").context("Failed to get XDG_DATA_HOME")?;
+
+            md5::compute(
+                format!(
+                    "{user_name}-{}",
+                    PathBuf::from(base_dir)
+                        .join("qutebrowser")
+                        .join(selected_project.to_project_display())
+                        .display()
+                )
+                .as_bytes(),
+            )
+        }));
+
+        if ipc_socket_path.exists() {
+            let mut stream = UnixStream::connect(ipc_socket_path)?;
+
+            let real_url = if let Some(url) = url {
+                url.to_string()
+            } else {
+                // Always add a new tab, so that qutebrowser is marked as “urgent”.
+                "qute://start".to_owned()
+            };
+
+            stream.write_all(
+                json! {
+                    {
+                        "args": [real_url],
+                        "target_arg": null,
+                        "version": "1.0.4",
+                        "protocol_version": 1,
+                        "cwd": "/"
+                    }
+                }
+                .to_string()
+                .as_bytes(),
+            )?;
+            stream.write_all(b"\n")?;
+
+            ExitStatus::default()
+        } else {
+            let args = if let Some(url) = url {
+                &[url.to_string()][..]
+            } else {
+                &[][..]
+            };
+
+            process::Command::new(format!(
+                "qutebrowser-{}",
+                selected_project.to_project_display()
+            ))
+            .args(args)
+            .status()
+            .context("Failed to start qutebrowser")?
+        }
+    };
+
+    if !status.success() {
+        error!("Qutebrowser 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 activate 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/cli.rs b/pkgs/by-name/ts/tskm/src/cli.rs
index f38d7879..90d6023b 100644
--- a/pkgs/by-name/ts/tskm/src/cli.rs
+++ b/pkgs/by-name/ts/tskm/src/cli.rs
@@ -11,12 +11,15 @@
 use std::{ffi::OsStr, path::PathBuf};
 
 use anyhow::{bail, Result};
-use clap::{builder::StyledStr, ArgAction, Parser, Subcommand};
+use clap::{builder::StyledStr, ArgAction, Parser, Subcommand, ValueEnum};
 use clap_complete::{ArgValueCompleter, CompletionCandidate};
 use url::Url;
 
 use crate::{
-    interface::{input::Input, project::ProjectName},
+    interface::{
+        input::{Input, Tag},
+        project::ProjectName,
+    },
     state, task,
 };
 
@@ -25,7 +28,7 @@ use crate::{
 /// 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”
+/// - `taskwarrior` projects are connected to `qutebrowser` profiles, making it possible to “open”
 ///   a project.
 ///
 /// - Every `taskwarrior` project has a determined `neorg` path, so that extra information for a
@@ -66,7 +69,7 @@ pub enum Command {
         command: NeorgCommand,
     },
 
-    /// Interface with the Firefox profile of each project.
+    /// Interface with the Qutebrowser profile of each project.
     Open {
         #[command(subcommand)]
         command: OpenCommand,
@@ -91,8 +94,8 @@ 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, add = ArgValueCompleter::new(complete_task_id))]
-        id: task::Task,
+        #[arg(value_name = "ID", value_parser = task_from_working_set_id, add = ArgValueCompleter::new(complete_task_id))]
+        task: task::Task,
     },
 }
 
@@ -108,12 +111,16 @@ fn task_from_working_set_id(id: &str) -> Result<task::Task> {
 
 #[derive(Subcommand, Debug)]
 pub enum OpenCommand {
-    /// Open each project's Firefox profile consecutively, that was opened since the last review.
+    /// Open each project's Qutebrowser 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,
+    Review {
+        /// Review all projects, if they contain tabs
+        #[arg(short, long, default_value_t)]
+        non_empty: bool,
+    },
 
-    /// Opens Firefox with either the supplied project or the currently active project profile.
+    /// Opens Qutebrowser with either the supplied project or the currently active project profile.
     Project {
         /// The project to open.
         #[arg(value_parser = task::Project::from_project_string, add = ArgValueCompleter::new(complete_project))]
@@ -123,7 +130,7 @@ pub enum OpenCommand {
         url: Option<Url>,
     },
 
-    /// Open a selected project in it's Firefox profile.
+    /// Open a selected project in it's Qutebrowser profile.
     ///
     /// This will use rofi's dmenu mode to select one project from the list of all registered
     /// projects.
@@ -134,12 +141,25 @@ pub enum OpenCommand {
 
     /// List all open tabs in the project.
     ListTabs {
-        /// The project to open.
+        /// The projects to open.
         #[arg(value_parser = task::Project::from_project_string, add = ArgValueCompleter::new(complete_project))]
-        project: Option<task::Project>,
+        projects: Option<Vec<task::Project>>,
+
+        /// Only show the tabs, that are in this mode
+        #[arg(short, long, conflicts_with = "projects")]
+        mode: Option<ListMode>,
     },
 }
 
+#[derive(Clone, Copy, ValueEnum, Debug)]
+pub enum ListMode {
+    // The tab contains no tabs.
+    Empty,
+
+    // The tab contains tabs.
+    NonEmpty,
+}
+
 #[derive(Subcommand, Debug)]
 pub enum InputCommand {
     /// Add URLs as inputs to be categorized.
@@ -153,7 +173,14 @@ pub enum InputCommand {
     /// Add all URLs in the file as inputs to be categorized.
     ///
     /// This expects each line to contain one URL.
-    File { file: PathBuf },
+    File {
+        /// The file to read from.
+        file: PathBuf,
+
+        /// Additional tags to apply to every read URL in the file.
+        #[arg(add = ArgValueCompleter::new(complete_tag))]
+        tags: Vec<Tag>,
+    },
 
     /// Like 'review', but for the inputs that have previously been added.
     /// It takes a project in which to open the URLs.
@@ -164,7 +191,14 @@ pub enum InputCommand {
     },
 
     /// List all the previously added inputs.
-    List,
+    List {
+        /// Only list the inputs that have all the specified tags
+        #[arg(add = ArgValueCompleter::new(complete_tag))]
+        tags: Vec<Tag>,
+    },
+
+    /// Show all the available tags.
+    Tags {},
 }
 
 fn complete_task_id(current: &OsStr) -> Vec<CompletionCandidate> {
@@ -219,8 +253,6 @@ fn complete_task_id(current: &OsStr) -> Vec<CompletionCandidate> {
                 if project == current_project {
                     if let Some(out) = format_task(task, current, &mut state) {
                         output.push(out);
-                    } else {
-                        continue;
                     }
                 }
             }
@@ -274,3 +306,27 @@ fn complete_input_url(current: &OsStr) -> Vec<CompletionCandidate> {
 
     output
 }
+fn complete_tag(current: &OsStr) -> Vec<CompletionCandidate> {
+    let mut output = vec![];
+
+    let Some(current) = current.to_str() else {
+        return output;
+    };
+
+    if !current.starts_with('+') {
+        output.push(CompletionCandidate::new(format!("+{current}")));
+    }
+
+    output
+}
+
+#[cfg(test)]
+mod test {
+    use clap::CommandFactory;
+
+    use super::CliArgs;
+    #[test]
+    fn verify_cli() {
+        CliArgs::command().debug_assert();
+    }
+}
diff --git a/pkgs/by-name/ts/tskm/src/interface/input/handle.rs b/pkgs/by-name/ts/tskm/src/interface/input/handle.rs
index 9c39cfef..11304633 100644
--- a/pkgs/by-name/ts/tskm/src/interface/input/handle.rs
+++ b/pkgs/by-name/ts/tskm/src/interface/input/handle.rs
@@ -9,25 +9,25 @@
 // If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
 
 use std::{
-    fs, process,
+    collections::{HashMap, HashSet},
+    fs,
     str::FromStr,
-    thread::{self, sleep},
-    time::Duration,
 };
 
 use anyhow::{Context, Result};
-use log::{error, info};
+use log::info;
 
-use crate::cli::InputCommand;
+use crate::{browser::open_in_browser, cli::InputCommand, state::State};
 
-use super::Input;
+use super::{Input, Tag};
 
 /// # Errors
 /// When command handling fails.
 ///
 /// # Panics
 /// When internal assertions fail.
-pub fn handle(command: InputCommand) -> Result<()> {
+#[allow(clippy::too_many_lines)]
+pub fn handle(command: InputCommand, state: &mut State) -> Result<()> {
     match command {
         InputCommand::Add { inputs } => {
             for input in inputs {
@@ -43,46 +43,35 @@ pub fn handle(command: InputCommand) -> Result<()> {
                 })?;
             }
         }
-        InputCommand::File { file } => {
-            let file = fs::read_to_string(file)?;
-            for line in file.lines() {
-                let input = Input::from_str(line)?;
+        InputCommand::File { file, tags } => {
+            let file = fs::read_to_string(&file)
+                .with_context(|| format!("Failed to read input file '{}'", file.display()))?;
+
+            let mut tag_set = HashSet::with_capacity(tags.len());
+            for tag in tags {
+                tag_set.insert(tag);
+            }
+
+            for line in file.lines().map(str::trim) {
+                if line.is_empty() {
+                    continue;
+                }
+
+                let mut input = Input::from_str(line)?;
+                input.tags = input.tags.union(&tag_set).cloned().collect();
+
                 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!");
-                    }
+                    open_in_browser(&project, state, Some(input.url.clone()))?;
                 }
 
                 {
@@ -108,15 +97,51 @@ pub fn handle(command: InputCommand) -> Result<()> {
                     }
                 }
             }
-
-            info!("Waiting for firefox to stop");
-            handle.join().expect("Should be joinable")?;
         }
-        InputCommand::List => {
-            for url in Input::all()? {
+        InputCommand::List { tags } => {
+            let mut tag_set = HashSet::with_capacity(tags.len());
+            for tag in tags {
+                tag_set.insert(tag);
+            }
+
+            for url in Input::all()?
+                .iter()
+                .filter(|input| tag_set.is_subset(&input.tags))
+            {
                 println!("{url}");
             }
         }
+        InputCommand::Tags {} => {
+            let mut without_tags = 0;
+            let mut tag_set: HashMap<Tag, u64> = HashMap::new();
+
+            for input in Input::all()? {
+                if input.tags.is_empty() {
+                    without_tags += 1;
+                }
+
+                for tag in input.tags {
+                    if let Some(number) = tag_set.get_mut(&tag) {
+                        *number += 1;
+                    } else {
+                        tag_set.insert(tag, 1);
+                    }
+                }
+            }
+
+            let mut tags: Vec<(Tag, u64)> = tag_set.into_iter().collect();
+            tags.sort_by_key(|(_, number)| *number);
+            tags.reverse();
+
+            for (tag, number) in tags {
+                println!("{tag} {number}");
+            }
+
+            if without_tags != 0 {
+                println!();
+                println!("Witohut tags: {without_tags}");
+            }
+        }
     }
     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
index 5485de3b..1d1d67f4 100644
--- a/pkgs/by-name/ts/tskm/src/interface/input/mod.rs
+++ b/pkgs/by-name/ts/tskm/src/interface/input/mod.rs
@@ -9,9 +9,9 @@
 // If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
 
 use std::{
-    collections::HashSet,
+    collections::{HashMap, HashSet},
     fmt::Display,
-    fs::{self, read_to_string, File},
+    fs,
     io::Write,
     path::PathBuf,
     process::Command,
@@ -26,38 +26,47 @@ pub mod handle;
 pub use handle::handle;
 
 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub struct NoWhitespaceString(String);
+pub struct Tag(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.")
+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 {
-            Self(input)
+            bail!("Your tag '{s}' does not start with the required '+'");
         }
     }
 }
 
-impl Display for NoWhitespaceString {
+impl Display for Tag {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        self.0.fmt(f)
+        write!(f, "+{}", self.0)
     }
 }
 
-impl NoWhitespaceString {
+impl Tag {
     #[must_use]
     pub fn as_str(&self) -> &str {
         &self.0
     }
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub struct Input {
     url: Url,
-    tags: HashSet<NoWhitespaceString>,
+    tags: HashSet<Tag>,
 }
 
 impl FromStr for Input {
@@ -71,13 +80,7 @@ impl FromStr for Input {
                 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 '+'");
-                            }
-                        })
+                        .map(Tag::new)
                         .collect::<Result<_, _>>()?
                 },
             })
@@ -101,13 +104,9 @@ impl Display for Input {
                 self.url,
                 self.tags
                     .iter()
-                    .fold(String::new(), |mut acc, tag| {
-                        acc.push('+');
-                        acc.push_str(tag.as_str());
-                        acc.push(' ');
-                        acc
-                    })
-                    .trim()
+                    .map(ToString::to_string)
+                    .collect::<Vec<_>>()
+                    .join(" ")
             )
         }
     }
@@ -123,7 +122,10 @@ impl Input {
     fn url_path(url: &Url) -> Result<PathBuf> {
         let base_path = Self::base_path();
 
-        let url_path = base_path.join(url.to_string());
+        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()))?;
 
@@ -142,17 +144,12 @@ impl Input {
     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)
+        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, "{url_content}{self}")?;
+        writeln!(file, "{self}")?;
 
         Self::git_commit(&format!("Add new url: '{self}'"))?;
 
@@ -183,28 +180,7 @@ impl Input {
         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.
+    /// Get all previously [committed][`Self::commit`] inputs.
     ///
     /// # Errors
     /// When IO handling fails.
@@ -227,41 +203,58 @@ impl Input {
                 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 });
+            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(())
+    }
 }
diff --git a/pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs b/pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs
index 84abb273..ea3a89ae 100644
--- a/pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs
+++ b/pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs
@@ -21,8 +21,8 @@ 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)?;
+        NeorgCommand::Task { task } => {
+            let project = task.project(state)?;
             let base = dirs::data_local_dir()
                 .expect("This should exists")
                 .join("tskm/notes");
@@ -40,15 +40,17 @@ pub fn handle(command: NeorgCommand, state: &mut State) -> Result<()> {
                     String::new()
                 };
 
-                if !contents.contains(format!("% {}", id.uuid()).as_str()) {
+                if !contents.contains(format!("% {}", task.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.write_all(
+                        format!("* {} (% {})", task.description(state)?, task.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()))?;
                 }
@@ -59,7 +61,7 @@ pub fn handle(command: NeorgCommand, state: &mut State) -> Result<()> {
                 .args([
                     path.to_str().expect("Should be a utf-8 str"),
                     "-c",
-                    format!("/% {}", id.uuid()).as_str(),
+                    format!("/% {}", task.uuid()).as_str(),
                 ])
                 .status()?;
             if !status.success() {
@@ -90,7 +92,7 @@ pub fn handle(command: NeorgCommand, state: &mut State) -> Result<()> {
             }
 
             {
-                id.mark_neorg_data(state)?;
+                task.mark_neorg_data(state)?;
             }
         }
     }
diff --git a/pkgs/by-name/ts/tskm/src/interface/neorg/mod.rs b/pkgs/by-name/ts/tskm/src/interface/neorg/mod.rs
index ed24c2cb..6bed1e39 100644
--- a/pkgs/by-name/ts/tskm/src/interface/neorg/mod.rs
+++ b/pkgs/by-name/ts/tskm/src/interface/neorg/mod.rs
@@ -12,7 +12,7 @@ use std::path::PathBuf;
 
 use anyhow::Result;
 
-use crate::task::{run_task, Project};
+use crate::task::{Project, run_task};
 
 pub mod handle;
 pub use handle::handle;
diff --git a/pkgs/by-name/ts/tskm/src/interface/open/handle.rs b/pkgs/by-name/ts/tskm/src/interface/open/handle.rs
index f5925523..ca54b422 100644
--- a/pkgs/by-name/ts/tskm/src/interface/open/handle.rs
+++ b/pkgs/by-name/ts/tskm/src/interface/open/handle.rs
@@ -8,37 +8,46 @@
 // 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::{
-    fs,
-    net::{IpAddr, Ipv4Addr},
-    path::PathBuf,
-    process,
-};
-
 use anyhow::{bail, Context, Result};
-use log::{error, info, warn};
+use log::{error, info};
 use url::Url;
 
-use crate::{cli::OpenCommand, rofi, state::State, task};
+use crate::{browser::open_in_browser, cli::OpenCommand, rofi, state::State, task};
+
+fn is_empty(project: &task::Project) -> Result<bool> {
+    let tabs = get_tabs(project)?;
+
+    Ok(tabs.is_empty())
+}
 
+#[allow(clippy::too_many_lines)]
 pub fn handle(command: OpenCommand, state: &mut State) -> Result<()> {
     match command {
-        OpenCommand::Review => {
+        OpenCommand::Review { non_empty } => {
             for project in task::Project::all().context("Failed to get all project files")? {
-                if project.is_touched() {
-                    info!("Reviewing project: '{}'", project.to_project_display());
+                let is_empty = is_empty(project)?;
+
+                if project.is_touched() || (non_empty && !is_empty) {
+                    info!(
+                        "Reviewing project: '{}' ({})",
+                        project.to_project_display(),
+                        if is_empty { "is empty" } else { "is not empty" }
+                    );
                     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 ('{}')",
+                            "Failed to open project ('{}') in qutebrowser",
                             project.to_project_display()
                         )
                     })?;
+
+                    if project.is_touched() {
+                        project.untouch().with_context(|| {
+                            format!(
+                                "Failed to untouch project ('{}')",
+                                project.to_project_display()
+                            )
+                        })?;
+                    }
                 }
             }
         }
@@ -68,175 +77,104 @@ pub fn handle(command: OpenCommand, state: &mut State) -> Result<()> {
 
             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!");
+        OpenCommand::ListTabs { projects, mode } => {
+            let projects = {
+                if let Some(p) = projects {
+                    p
+                } else if mode.is_some() {
+                    task::Project::all()
+                        .context("Failed to get all projects")?
+                        .to_owned()
+                } else if let Some(p) = task::Project::get_current()
+                    .context("Failed to get currently focused project")?
+                {
+                    vec![p]
+                } else {
+                    bail!("You need to either select projects or pass --mode");
+                }
             };
 
-            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 {
-                        "   "
+            for project in &projects {
+                if let Some(mode) = mode {
+                    match mode {
+                        crate::cli::ListMode::Empty => {
+                            if !is_empty(project)? {
+                                continue;
+                            }
+
+                            // We do not need to print, tabs they are always empty.
+                            if projects.len() > 1 {
+                                println!("/* {} */", project.to_project_display());
+                            }
+                            continue;
+                        }
+                        crate::cli::ListMode::NonEmpty => {
+                            if is_empty(project)? {
+                                continue;
+                            }
+                        }
                     }
-                };
-                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 lock_file = dirs::home_dir()
-                .expect("Exists")
-                .join(".mozilla/firefox")
-                .join(selected_project.to_project_display())
-                .join("lock");
-
-            if lock_file.exists() {
-                let (ip, pid): (IpAddr, u32) = {
-                    let link = fs::read_link(&lock_file).with_context(|| {
-                        format!("Failed to readlink lock at '{}'", lock_file.display())
-                    })?;
+                }
 
-                    let (ip, pid) = link
-                        .to_str()
-                        .expect("Should work")
-                        .split_once(':')
-                        .expect("The split works");
+                if projects.len() > 1 {
+                    println!("/* {} */", project.to_project_display());
+                }
 
-                    (
-                        ip.parse().expect("Should be a valid ip address"),
-                        pid.parse().expect("Should be a valid pid"),
-                    )
+                let tabs = match get_tabs(project) {
+                    Ok(ok) => ok,
+                    Err(err) => {
+                        if projects.len() > 1 {
+                            error!(
+                                "While trying to get the sessionstore for {}: {:?}",
+                                project.to_project_display(),
+                                err
+                            );
+                            continue;
+                        }
+
+                        return Err(err).with_context(|| {
+                            format!(
+                                "While trying to get the sessionstore for {}",
+                                project.to_project_display()
+                            )
+                        });
+                    }
                 };
 
-                if ip != Ipv4Addr::new(127, 0, 0, 2) {
-                    warn!("Your ip is weird..");
+                for (active, url) in tabs {
+                    let is_selected = {
+                        if active {
+                            "🔻 "
+                        } else {
+                            "   "
+                        }
+                    };
+                    println!("{is_selected}{url}");
                 }
-
-                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.
-                }
-            } else {
-                // There is no lock file and thus no instance already open.
             }
-        };
-
-        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}"))?;
-    }
+    Ok(())
+}
 
-    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")?;
-    }
+fn get_tabs(project: &task::Project) -> Result<Vec<(bool, Url)>> {
+    let session_store = project.get_sessionstore()?;
 
-    Ok(())
+    let tabs = session_store
+        .windows
+        .iter()
+        .flat_map(|window| window.tabs.iter())
+        .filter_map(|tab| {
+            tab.history
+                .iter()
+                .find(|hist| hist.active)
+                .map(|hist| (tab.active, hist))
+        })
+        .collect::<Vec<_>>();
+
+    Ok(tabs
+        .into_iter()
+        .map(|(active, hist)| (active, hist.url.clone()))
+        .collect())
 }
diff --git a/pkgs/by-name/ts/tskm/src/interface/open/mod.rs b/pkgs/by-name/ts/tskm/src/interface/open/mod.rs
index a4060fa3..40e057c1 100644
--- a/pkgs/by-name/ts/tskm/src/interface/open/mod.rs
+++ b/pkgs/by-name/ts/tskm/src/interface/open/mod.rs
@@ -8,13 +8,12 @@
 // 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, fs::File, io};
+use std::{fs::File, io::Read, str::FromStr};
 
-use anyhow::{Context, Result};
-use lz4_flex::decompress_size_prepended;
-use serde::Deserialize;
-use serde_json::Value;
+use anyhow::{anyhow, Context, Result};
+use taskchampion::chrono::NaiveDateTime;
 use url::Url;
+use yaml_rust2::Yaml;
 
 use crate::task::Project;
 
@@ -23,94 +22,151 @@ 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")
+        let path = dirs::data_local_dir()
+            .context("Failed to get data dir")?
+            .join("qutebrowser")
             .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";
+            // NOTE(@bpeetz): We could use another real session name, but this file should
+            // always exist. <2025-06-03>
+            .join("data/sessions/_autosave.yml");
 
-    let mut buf = [0u8; 8];
-    file.read_exact(&mut buf)
-        .context("Failed to read the mozlz40 header.")?;
+        let mut file = File::open(&path)
+            .with_context(|| format!("Failed to open path '{}'", path.display()))?;
 
-    assert_eq!(buf, MOZLZ4_MAGIC_NUMBER);
+        let mut yaml_str = String::new();
+        file.read_to_string(&mut yaml_str)
+            .context("Failed to read _autosave.yml path")?;
+        let yaml = yaml_rust2::YamlLoader::load_from_str(&yaml_str)?;
 
-    let mut buf = vec![];
-    file.read_to_end(&mut buf).context("Failed to read file")?;
+        let store = qute_store_from_yaml(&yaml).context("Failed to read yaml store")?;
 
-    let uncompressed = decompress_size_prepended(&buf).context("Failed to decompress file")?;
+        Ok(store)
+    }
+}
 
-    Ok(String::from_utf8(uncompressed).expect("This should be valid json and thus utf8"))
+fn qute_store_from_yaml(yaml: &[Yaml]) -> Result<SessionStore> {
+    assert_eq!(yaml.len(), 1);
+    let doc = &yaml[0];
+
+    let hash = doc.as_hash().context("Invalid yaml")?;
+    let windows = hash
+        .get(&Yaml::String("windows".to_owned()))
+        .ok_or(anyhow!("Missing windows"))?
+        .as_vec()
+        .ok_or(anyhow!("Windows not vector"))?;
+
+    Ok(SessionStore {
+        windows: windows
+            .iter()
+            .map(|window| {
+                let hash = window.as_hash().ok_or(anyhow!("Windows not hashmap"))?;
+
+                Ok::<_, anyhow::Error>(Window {
+                    geometry: hash
+                        .get(&Yaml::String("geometry".to_owned()))
+                        .ok_or(anyhow!("Missing window geometry"))?
+                        .as_str()
+                        .ok_or(anyhow!("geometry not string"))?
+                        .to_owned(),
+                    tabs: hash
+                        .get(&Yaml::String("tabs".to_owned()))
+                        .ok_or(anyhow!("Missing window tabs"))?
+                        .as_vec()
+                        .ok_or(anyhow!("Tabs not vec"))?
+                        .iter()
+                        .map(|tab| {
+                            let hash = tab.as_hash().ok_or(anyhow!("Tab not hashmap"))?;
+
+                            Ok::<_, anyhow::Error>(Tab {
+                                history: hash
+                                    .get(&Yaml::String("history".to_owned()))
+                                    .ok_or(anyhow!("Missing tab history"))?
+                                    .as_vec()
+                                    .ok_or(anyhow!("tab history not vec"))?
+                                    .iter()
+                                    .map(|history| {
+                                        let hash = history
+                                            .as_hash()
+                                            .ok_or(anyhow!("Tab history not hashmap"))?;
+
+                                        Ok::<_, anyhow::Error>(TabHistory {
+                                            active: hash
+                                                .get(&Yaml::String("active".to_owned()))
+                                                .ok_or(anyhow!("Missing tab history active"))?
+                                                .as_bool()
+                                                .ok_or(anyhow!("tab history active not bool"))?,
+                                            last_visited: NaiveDateTime::from_str(
+                                                hash.get(&Yaml::String("last_visited".to_owned()))
+                                                    .ok_or(anyhow!(
+                                                        "Missing tab history last_visited"
+                                                    ))?
+                                                    .as_str()
+                                                    .ok_or(anyhow!(
+                                                        "tab history last_visited not string"
+                                                    ))?,
+                                            )
+                                            .context("Failed to parse last_visited")?,
+                                            pinned: hash
+                                                .get(&Yaml::String("pinned".to_owned()))
+                                                .ok_or(anyhow!("Missing tab history pinned"))?
+                                                .as_bool()
+                                                .ok_or(anyhow!("tab history pinned not bool"))?,
+                                            title: hash
+                                                .get(&Yaml::String("title".to_owned()))
+                                                .ok_or(anyhow!("Missing tab history title"))?
+                                                .as_str()
+                                                .ok_or(anyhow!("tab history title not string"))?
+                                                .to_owned(),
+                                            url: Url::parse(
+                                                hash.get(&Yaml::String("url".to_owned()))
+                                                    .ok_or(anyhow!("Missing tab history url"))?
+                                                    .as_str()
+                                                    .ok_or(anyhow!("tab history url not string"))?,
+                                            )
+                                            .context("Failed to parse url")?,
+                                            zoom: hash
+                                                .get(&Yaml::String("zoom".to_owned()))
+                                                .ok_or(anyhow!("Missing tab history zoom"))?
+                                                .as_f64()
+                                                .ok_or(anyhow!("tab history zoom not 64"))?,
+                                        })
+                                    })
+                                    .collect::<Result<Vec<_>, _>>()?,
+                                active: hash
+                                    .get(&Yaml::String("active".to_owned()))
+                                    .unwrap_or(&Yaml::Boolean(false))
+                                    .as_bool()
+                                    .ok_or(anyhow!("active not bool"))?,
+                            })
+                        })
+                        .collect::<Result<Vec<_>, _>>()?,
+                })
+            })
+            .collect::<Result<Vec<_>, _>>()?,
+    })
 }
 
-#[derive(Deserialize, Debug)]
+#[derive(Debug)]
 pub struct SessionStore {
     pub windows: Vec<Window>,
 }
-
-#[derive(Deserialize, Debug)]
+#[derive(Debug)]
 pub struct Window {
+    pub geometry: String,
     pub tabs: Vec<Tab>,
-    pub selected: usize,
 }
-
-#[derive(Deserialize, Debug)]
+#[derive(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>,
+    pub history: Vec<TabHistory>,
+    pub active: bool,
 }
-
-#[derive(Deserialize, Debug)]
-pub struct TabEntry {
-    pub url: Url,
+#[derive(Debug)]
+pub struct TabHistory {
+    pub active: bool,
+    pub last_visited: NaiveDateTime,
+    pub pinned: bool,
+    // pub scroll-pos:
     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,
+    pub url: Url,
+    pub zoom: f64,
 }
-
-#[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
index c698500f..6d44b340 100644
--- a/pkgs/by-name/ts/tskm/src/interface/project/handle.rs
+++ b/pkgs/by-name/ts/tskm/src/interface/project/handle.rs
@@ -10,7 +10,7 @@
 
 use std::{env, fs::File, io::Write};
 
-use anyhow::{anyhow, Context, Result};
+use anyhow::{Context, Result, anyhow};
 use log::trace;
 
 use crate::{cli::ProjectCommand, task};
@@ -70,10 +70,12 @@ pub fn handle(command: ProjectCommand) -> Result<()> {
 
                     let new_definition = ProjectDefinition::default();
 
-                    assert!(definition
-                        .subprojects
-                        .insert(segment.clone(), new_definition)
-                        .is_none());
+                    assert!(
+                        definition
+                            .subprojects
+                            .insert(segment.clone(), new_definition)
+                            .is_none()
+                    );
 
                     definition = definition
                         .subprojects
diff --git a/pkgs/by-name/ts/tskm/src/main.rs b/pkgs/by-name/ts/tskm/src/main.rs
index fe011e27..e6113111 100644
--- a/pkgs/by-name/ts/tskm/src/main.rs
+++ b/pkgs/by-name/ts/tskm/src/main.rs
@@ -17,6 +17,7 @@ use crate::{
     state::State,
 };
 
+pub mod browser;
 pub mod cli;
 pub mod interface;
 pub mod rofi;
@@ -34,14 +35,13 @@ fn main() -> Result<(), anyhow::Error> {
         .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::Inputs { command } => input::handle(command, &mut state)?,
         Command::Neorg { command } => neorg::handle(command, &mut state)?,
         Command::Open { command } => open::handle(command, &mut state)?,
         Command::Projects { command } => project::handle(command)?,
diff --git a/pkgs/by-name/ts/tskm/src/state.rs b/pkgs/by-name/ts/tskm/src/state.rs
index 3c611d8f..ae71764e 100644
--- a/pkgs/by-name/ts/tskm/src/state.rs
+++ b/pkgs/by-name/ts/tskm/src/state.rs
@@ -11,7 +11,7 @@
 use std::path::PathBuf;
 
 use anyhow::Result;
-use taskchampion::{storage::AccessMode, Replica, StorageConfig};
+use taskchampion::{Replica, StorageConfig, storage::AccessMode};
 
 pub struct State {
     replica: Replica,
diff --git a/pkgs/by-name/ts/tskm/src/task/mod.rs b/pkgs/by-name/ts/tskm/src/task/mod.rs
index 7d00896f..9c671273 100644
--- a/pkgs/by-name/ts/tskm/src/task/mod.rs
+++ b/pkgs/by-name/ts/tskm/src/task/mod.rs
@@ -76,7 +76,6 @@ impl Task {
     pub fn uuid(&self) -> &taskchampion::Uuid {
         &self.uuid
     }
-    #[must_use]
     pub fn working_set_id(&self, state: &mut State) -> Result<usize> {
         Ok(state
             .replica()
@@ -356,5 +355,13 @@ pub(crate) fn run_task(args: &[&str]) -> Result<String> {
     trace!("Output (stdout): '{}'", stdout.trim());
     trace!("Output (stderr): '{}'", stderr.trim());
 
+    if !output.status.success() {
+        bail!(
+            "Command `task {}` failed with status: {}",
+            args.join(" "),
+            output.status
+        );
+    }
+
     Ok(stdout.trim().to_owned())
 }