aboutsummaryrefslogtreecommitdiffstats
path: root/pkgs/by-name/ts/tskm/src/interface
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/by-name/ts/tskm/src/interface')
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/input/handle.rs131
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/input/mod.rs189
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/mod.rs10
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs45
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/neorg/mod.rs21
-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.rs240
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/project/handle.rs22
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/project/mod.rs10
9 files changed, 582 insertions, 378 deletions
diff --git a/pkgs/by-name/ts/tskm/src/interface/input/handle.rs b/pkgs/by-name/ts/tskm/src/interface/input/handle.rs
index 0ff0e56e..cd868f7a 100644
--- a/pkgs/by-name/ts/tskm/src/interface/input/handle.rs
+++ b/pkgs/by-name/ts/tskm/src/interface/input/handle.rs
@@ -1,23 +1,34 @@
+// nixos-config - My current NixOS configuration
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of my nixos-config.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
use std::{
- 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 taskchampion::chrono::Utc;
-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 async fn handle(command: InputCommand, state: &mut State) -> Result<()> {
match command {
InputCommand::Add { inputs } => {
for input in inputs {
@@ -33,47 +44,43 @@ 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);
+ }
+
+ tag_set.insert(
+ Tag::new(format!("+{}", Utc::now().format("%Y-%m-%d")).as_str())
+ .expect("hardcoded"),
+ );
+
+ 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(all.iter().map(|f| f.url.clone()).collect()),
+ )
+ .await?;
{
use std::io::{stdin, stdout, Write};
@@ -98,15 +105,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 9ece7a3a..b6176a96 100644
--- a/pkgs/by-name/ts/tskm/src/interface/input/mod.rs
+++ b/pkgs/by-name/ts/tskm/src/interface/input/mod.rs
@@ -1,14 +1,24 @@
+// nixos-config - My current NixOS configuration
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of my nixos-config.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
use std::{
- collections::HashSet,
+ collections::{HashMap, HashSet},
fmt::Display,
- fs::{self, read_to_string, File},
+ fs,
io::Write,
path::PathBuf,
process::Command,
str::FromStr,
};
-use anyhow::{bail, Context, Result};
+use anyhow::{Context, Result, bail};
use url::Url;
use walkdir::WalkDir;
@@ -16,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 {
@@ -61,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<_, _>>()?
},
})
@@ -91,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(" ")
)
}
}
@@ -113,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()))?;
@@ -132,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}'"))?;
@@ -173,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.
@@ -217,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_value_file = entry.path();
+ 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 url_values = fs::read_to_string(PathBuf::from(url_value_file))?;
- let (proto, path) = base.split_once(':').expect("This will countain a :");
+ 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);
+ }
+ }
- let path = path.strip_suffix("/url_value").expect("Will exist");
+ output.extend(inputs.drain().map(|(_, value)| value));
+ }
- 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()
- };
+ Ok(output)
+ }
- output.push(Self { url, tags });
+ /// 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!");
+ }
}
- Ok(output)
+ 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/mod.rs b/pkgs/by-name/ts/tskm/src/interface/mod.rs
index 1a0d934c..513ca317 100644
--- a/pkgs/by-name/ts/tskm/src/interface/mod.rs
+++ b/pkgs/by-name/ts/tskm/src/interface/mod.rs
@@ -1,3 +1,13 @@
+// nixos-config - My current NixOS configuration
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of my nixos-config.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
pub mod input;
pub mod neorg;
pub mod open;
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 577de02c..12a0180d 100644
--- a/pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs
+++ b/pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs
@@ -1,3 +1,13 @@
+// nixos-config - My current NixOS configuration
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of my nixos-config.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
use std::{
env,
fs::{self, read_to_string, File, OpenOptions},
@@ -9,14 +19,14 @@ use anyhow::{bail, Context, Result};
use crate::{cli::NeorgCommand, state::State};
-pub fn handle(command: NeorgCommand, state: &mut State) -> Result<()> {
+pub async fn handle(command: NeorgCommand, state: &mut State) -> Result<()> {
match command {
- NeorgCommand::Task { id } => {
- let project = id.project(state)?;
- let path = dirs::data_local_dir()
+ NeorgCommand::Task { task } => {
+ let project = task.project(state).await?;
+ let base = dirs::data_local_dir()
.expect("This should exists")
- .join("tskm/notes")
- .join(project.get_neorg_path()?);
+ .join("tskm/notes");
+ let path = base.join(project.get_neorg_path()?);
fs::create_dir_all(path.parent().expect("This should exist"))?;
@@ -30,15 +40,18 @@ 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).await?, 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()))?;
}
@@ -49,7 +62,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() {
@@ -69,11 +82,7 @@ pub fn handle(command: NeorgCommand, state: &mut State) -> Result<()> {
.args([
"commit",
"--message",
- format!(
- "chore({}): Update",
- path.parent().expect("Should have a parent").display()
- )
- .as_str(),
+ format!("chore({}): Update", project.get_neorg_path()?.display()).as_str(),
"--no-gpg-sign",
])
.current_dir(path.parent().expect("Will exist"))
@@ -84,7 +93,7 @@ pub fn handle(command: NeorgCommand, state: &mut State) -> Result<()> {
}
{
- id.mark_neorg_data(state)?;
+ task.mark_neorg_data(state).await?;
}
}
}
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 dc5cdf19..6bed1e39 100644
--- a/pkgs/by-name/ts/tskm/src/interface/neorg/mod.rs
+++ b/pkgs/by-name/ts/tskm/src/interface/neorg/mod.rs
@@ -1,18 +1,35 @@
+// nixos-config - My current NixOS configuration
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of my nixos-config.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
use std::path::PathBuf;
use anyhow::Result;
-use crate::task::{run_task, Project};
+use crate::task::{Project, run_task};
pub mod handle;
pub use handle::handle;
impl Project {
+ /// Return the stored neorg path of this project.
+ /// The returned path will never start with a slash (/).
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()))
+
+ let final_path = project_path
+ .strip_prefix('/')
+ .unwrap_or(project_path.as_str());
+
+ Ok(PathBuf::from(final_path))
}
}
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 0b565abd..5b9100bc 100644
--- a/pkgs/by-name/ts/tskm/src/interface/open/handle.rs
+++ b/pkgs/by-name/ts/tskm/src/interface/open/handle.rs
@@ -1,48 +1,77 @@
-use std::process;
+// nixos-config - My current NixOS configuration
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of my nixos-config.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::str::FromStr;
use anyhow::{bail, Context, Result};
use log::{error, info};
+use url::Url;
+
+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)?;
-use crate::{cli::OpenCommand, rofi, state::State, task};
+ if tabs.is_empty() {
+ Ok(true)
+ } else if tabs.len() > 1 {
+ Ok(false)
+ } else {
+ let url = &tabs[0].1;
-pub fn handle(command: OpenCommand, state: &mut State) -> Result<()> {
+ Ok(url == &Url::from_str("qute://start/").expect("Hardcoded"))
+ }
+}
+
+#[allow(clippy::too_many_lines)]
+pub async 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());
- open_in_browser(project, state).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()
- )
- })?;
+ 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::<Vec<Url>>)
+ .await
+ .with_context(|| {
+ format!(
+ "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()
+ )
+ })?;
+ }
}
}
}
- OpenCommand::Project { 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::Project { project, urls } => {
project.touch().context("Failed to touch project")?;
- open_in_browser(&project, state).with_context(|| {
- format!("Failed to open project: {}", project.to_project_display())
- })?;
+ open_in_browser(&project, state, urls)
+ .await
+ .with_context(|| {
+ format!("Failed to open project: {}", project.to_project_display())
+ })?;
}
- OpenCommand::Select => {
+ OpenCommand::Select { urls } => {
let selected_project: task::Project = task::Project::from_project_string(
&rofi::select(
task::Project::all()
@@ -60,129 +89,108 @@ pub fn handle(command: OpenCommand, state: &mut State) -> Result<()> {
.touch()
.context("Failed to touch project")?;
- open_in_browser(&selected_project, state).context("Failed to open project")?;
+ open_in_browser(&selected_project, state, urls)
+ .await
+ .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()
- )
- })?;
+ for project in &projects {
+ if let Some(mode) = mode {
+ match mode {
+ crate::cli::ListMode::Empty => {
+ if !is_empty(project)? {
+ continue;
+ }
- let selected = session_store
- .windows
- .iter()
- .map(|w| w.selected)
- .collect::<Vec<_>>();
+ // 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;
+ }
+ }
+ }
+ }
- 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<_>>();
+ if projects.len() > 1 {
+ println!("/* {} */", project.to_project_display());
+ }
+
+ 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;
+ }
- for (index, entry) in tabs.iter().enumerate() {
- let index = index + 1;
- let is_selected = {
- if selected.contains(&index) {
- "🔻 "
- } else {
- " "
+ return Err(err).with_context(|| {
+ format!(
+ "While trying to get the sessionstore for {}",
+ project.to_project_display()
+ )
+ });
}
};
- println!("{}{}", is_selected, entry.url);
+
+ for (active, url) in tabs {
+ let is_selected = {
+ if active {
+ "🔻 "
+ } else {
+ " "
+ }
+ };
+ println!("{is_selected}{url}");
+ }
}
}
}
+
Ok(())
}
-fn open_in_browser(selected_project: &task::Project, state: &mut State) -> 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 = process::Command::new("firefox")
- .args([
- "-P",
- selected_project.to_project_display().as_str(),
- "about:newtab",
- ])
- .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}"))?;
- }
+fn get_tabs(project: &task::Project) -> Result<Vec<(bool, Url)>> {
+ let session_store = project.get_sessionstore()?;
- 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")?;
- }
+ 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(())
+ 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 2dc75957..407536d2 100644
--- a/pkgs/by-name/ts/tskm/src/interface/open/mod.rs
+++ b/pkgs/by-name/ts/tskm/src/interface/open/mod.rs
@@ -1,106 +1,198 @@
-use std::{collections::HashMap, fs::File, io};
+// nixos-config - My current NixOS configuration
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of my nixos-config.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-use anyhow::{Context, Result};
-use lz4_flex::decompress_size_prepended;
-use serde::Deserialize;
-use serde_json::Value;
+use std::{
+ fs::{self, File},
+ io::Read,
+ str::FromStr,
+};
+
+use anyhow::{Context, Result, anyhow};
+use taskchampion::chrono::NaiveDateTime;
use url::Url;
+use yaml_rust2::Yaml;
use crate::task::Project;
pub mod handle;
pub use handle::handle;
+/// An Url that also accepts file paths
+#[derive(Debug, Clone)]
+pub struct UrlLike(Url);
+
+impl FromStr for UrlLike {
+ type Err = url::ParseError;
+
+ fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
+ if let Ok(u) = fs::canonicalize(s) {
+ Ok(Self(Url::from_file_path(u).expect(
+ "The path could be canonicalized, as such it is valid for this",
+ )))
+ } else {
+ Url::from_str(s).map(Self)
+ }
+ }
+}
+
+impl From<UrlLike> for Url {
+ fn from(value: UrlLike) -> Self {
+ value.0
+ }
+}
+
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()))?;
+ .join("data/sessions/default.yml");
- let contents: SessionStore = serde_json::from_str(&file).with_context(|| {
- format!(
- "Failed to deserialize file ('{}') as session store.",
- path.display()
- )
- })?;
- Ok(contents)
+ let mut file = File::open(&path)
+ .with_context(|| format!("Failed to open path '{}'", path.display()))?;
+
+ 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 store = qute_store_from_yaml(&yaml).context("Failed to read yaml store")?;
+
+ Ok(store)
}
}
-fn decompress_mozlz4<P: io::Read>(mut file: P) -> Result<String> {
- const MOZLZ4_MAGIC_NUMBER: &[u8] = b"mozLz40\0";
+fn qute_store_from_yaml(yaml: &[Yaml]) -> Result<SessionStore> {
+ assert_eq!(yaml.len(), 1);
+ let doc = &yaml[0];
- let mut buf = [0u8; 8];
- file.read_exact(&mut buf)
- .context("Failed to read the mozlz40 header.")?;
+ 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"))?;
- assert_eq!(buf, MOZLZ4_MAGIC_NUMBER);
+ Ok(SessionStore {
+ windows: windows
+ .iter()
+ .map(|window| {
+ let hash = window.as_hash().ok_or(anyhow!("Windows not hashmap"))?;
- let mut buf = vec![];
- file.read_to_end(&mut buf).context("Failed to read file")?;
+ 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"))?;
- let uncompressed = decompress_size_prepended(&buf).context("Failed to decompress file")?;
+ 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(String::from_utf8(uncompressed).expect("This should be valid json and thus utf8"))
+ Ok::<_, anyhow::Error>(TabHistory {
+ active: hash
+ .get(&Yaml::String("active".to_owned()))
+ .unwrap_or(&Yaml::Boolean(false))
+ .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()))
+ .unwrap_or(&Yaml::Real("1.0".to_owned()))
+ .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 2b01f5d1..6d44b340 100644
--- a/pkgs/by-name/ts/tskm/src/interface/project/handle.rs
+++ b/pkgs/by-name/ts/tskm/src/interface/project/handle.rs
@@ -1,6 +1,16 @@
+// nixos-config - My current NixOS configuration
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of my nixos-config.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
use std::{env, fs::File, io::Write};
-use anyhow::{anyhow, Context, Result};
+use anyhow::{Context, Result, anyhow};
use log::trace;
use crate::{cli::ProjectCommand, task};
@@ -60,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/interface/project/mod.rs b/pkgs/by-name/ts/tskm/src/interface/project/mod.rs
index 62069746..8a7fa1b0 100644
--- a/pkgs/by-name/ts/tskm/src/interface/project/mod.rs
+++ b/pkgs/by-name/ts/tskm/src/interface/project/mod.rs
@@ -1,3 +1,13 @@
+// nixos-config - My current NixOS configuration
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of my nixos-config.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
use std::collections::HashMap;
use anyhow::Result;