diff options
Diffstat (limited to '')
-rw-r--r-- | pkgs/by-name/ts/tskm/src/browser/mod.rs | 172 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/cli.rs | 98 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/interface/input/handle.rs | 117 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/interface/input/mod.rs | 195 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/interface/mod.rs | 10 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs | 30 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/interface/neorg/mod.rs | 12 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/interface/open/handle.rs | 300 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/interface/open/mod.rs | 224 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/interface/project/handle.rs | 22 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/interface/project/mod.rs | 10 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/main.rs | 14 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/rofi/mod.rs | 10 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/state.rs | 12 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/task/mod.rs | 19 |
15 files changed, 818 insertions, 427 deletions
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 c1eba387..90d6023b 100644 --- a/pkgs/by-name/ts/tskm/src/cli.rs +++ b/pkgs/by-name/ts/tskm/src/cli.rs @@ -1,12 +1,25 @@ +// 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::{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, }; @@ -15,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 @@ -56,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, @@ -81,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, }, } @@ -98,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))] @@ -113,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. @@ -124,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. @@ -143,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. @@ -154,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> { @@ -209,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; } } } @@ -264,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 0ff0e56e..11304633 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,33 @@ +// 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 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 { @@ -33,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()))?; } { @@ -98,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 9ece7a3a..1d1d67f4 100644 --- a/pkgs/by-name/ts/tskm/src/interface/input/mod.rs +++ b/pkgs/by-name/ts/tskm/src/interface/input/mod.rs @@ -1,7 +1,17 @@ +// 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, @@ -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 = { - 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/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 d904b12e..ea3a89ae 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}, @@ -11,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"); @@ -30,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()))?; } @@ -49,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() { @@ -80,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 51d58ab3..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,8 +1,18 @@ +// 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; 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 4d7341b2..ca54b422 100644 --- a/pkgs/by-name/ts/tskm/src/interface/open/handle.rs +++ b/pkgs/by-name/ts/tskm/src/interface/open/handle.rs @@ -1,34 +1,53 @@ -use std::{ - fs, - net::{IpAddr, Ipv4Addr}, - path::PathBuf, - 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 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() + ) + })?; + } } } } @@ -58,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.."); - } - - 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. + for (active, url) in tabs { + let is_selected = { + if active { + "🔻 " + } else { + " " + } + }; + println!("{is_selected}{url}"); } - } 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 2dc75957..40e057c1 100644 --- a/pkgs/by-name/ts/tskm/src/interface/open/mod.rs +++ b/pkgs/by-name/ts/tskm/src/interface/open/mod.rs @@ -1,10 +1,19 @@ -use std::{collections::HashMap, fs::File, io}; - -use anyhow::{Context, Result}; -use lz4_flex::decompress_size_prepended; -use serde::Deserialize; -use serde_json::Value; +// 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::File, io::Read, str::FromStr}; + +use anyhow::{anyhow, Context, Result}; +use taskchampion::chrono::NaiveDateTime; use url::Url; +use yaml_rust2::Yaml; use crate::task::Project; @@ -13,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) - } -} + // NOTE(@bpeetz): We could use another real session name, but this file should + // always exist. <2025-06-03> + .join("data/sessions/_autosave.yml"); -fn decompress_mozlz4<P: io::Read>(mut file: P) -> Result<String> { - const MOZLZ4_MAGIC_NUMBER: &[u8] = b"mozLz40\0"; + let mut file = File::open(&path) + .with_context(|| format!("Failed to open path '{}'", path.display()))?; - let mut buf = [0u8; 8]; - file.read_exact(&mut buf) - .context("Failed to read the mozlz40 header.")?; + 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)?; - assert_eq!(buf, MOZLZ4_MAGIC_NUMBER); + let store = qute_store_from_yaml(&yaml).context("Failed to read yaml store")?; - let mut buf = vec![]; - file.read_to_end(&mut buf).context("Failed to read file")?; - - let uncompressed = decompress_size_prepended(&buf).context("Failed to decompress file")?; + Ok(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 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; diff --git a/pkgs/by-name/ts/tskm/src/main.rs b/pkgs/by-name/ts/tskm/src/main.rs index 77f2dcca..e6113111 100644 --- a/pkgs/by-name/ts/tskm/src/main.rs +++ b/pkgs/by-name/ts/tskm/src/main.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 anyhow::Result; use clap::{CommandFactory, Parser}; @@ -7,6 +17,7 @@ use crate::{ state::State, }; +pub mod browser; pub mod cli; pub mod interface; pub mod rofi; @@ -24,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/rofi/mod.rs b/pkgs/by-name/ts/tskm/src/rofi/mod.rs index a0591b7f..37c2eafa 100644 --- a/pkgs/by-name/ts/tskm/src/rofi/mod.rs +++ b/pkgs/by-name/ts/tskm/src/rofi/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::{ io::Write, process::{Command, Stdio}, diff --git a/pkgs/by-name/ts/tskm/src/state.rs b/pkgs/by-name/ts/tskm/src/state.rs index 175a7f03..ae71764e 100644 --- a/pkgs/by-name/ts/tskm/src/state.rs +++ b/pkgs/by-name/ts/tskm/src/state.rs @@ -1,7 +1,17 @@ +// 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 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 989f273a..9c671273 100644 --- a/pkgs/by-name/ts/tskm/src/task/mod.rs +++ b/pkgs/by-name/ts/tskm/src/task/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::{ fmt::Display, fs::{self, read_to_string, File}, @@ -66,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() @@ -346,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()) } |