diff --git a/CHANGELOG.md b/CHANGELOG.md index 82905e1..a64daf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `description` tag to the patch - by Awiteb - New `config pow` command to set the default PoW difficulty - by Awiteb - New `config relays` command to set the default fallbacks relays - by Awiteb +- New `issue view` command to view an issue - by Awiteb ### Refactor diff --git a/src/cli/commands/issue/mod.rs b/src/cli/commands/issue/mod.rs index 495c729..40268f9 100644 --- a/src/cli/commands/issue/mod.rs +++ b/src/cli/commands/issue/mod.rs @@ -16,10 +16,13 @@ /// `issue new` subcommand mod new; +/// `issue view` subcommand +mod view; use clap::Subcommand; use self::new::NewArgs; +use self::view::ViewArgs; use super::{CliOptions, CommandRunner}; use crate::error::N34Result; @@ -30,10 +33,12 @@ pub const ISSUE_ALT_PREFIX: &str = "git issue: "; pub enum IssueSubcommands { /// Create a new repository issue New(NewArgs), + /// View an issue by its ID + View(ViewArgs), } impl CommandRunner for IssueSubcommands { async fn run(self, options: CliOptions) -> N34Result<()> { - crate::run_command!(self, options, &New) + crate::run_command!(self, options, & New View) } } diff --git a/src/cli/commands/issue/view.rs b/src/cli/commands/issue/view.rs new file mode 100644 index 0000000..8fd0a0c --- /dev/null +++ b/src/cli/commands/issue/view.rs @@ -0,0 +1,111 @@ +// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr +// Copyright (C) 2025 Awiteb +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use clap::Args; +use nostr::{ + event::{Kind, TagKind}, + filter::{Alphabet, Filter}, +}; + +use crate::{ + cli::{ + CliOptions, + traits::CommandRunner, + types::{NaddrOrSet, NostrEvent, OptionNaddrOrSetVecExt, RelayOrSetVecExt}, + }, + error::{N34Error, N34Result}, + nostr_utils::{ + NostrClient, + traits::{NaddrsUtils, ReposUtils}, + utils, + }, +}; + +#[derive(Debug, Args)] +pub struct ViewArgs { + /// Repository address in `naddr` format (`naddr1...`), NIP-05 format + /// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`. + /// + /// If omitted, looks for a `nostr-address` file. + #[arg(value_name = "NADDR-NIP05-OR-SET", long = "repo")] + naddrs: Option>, + /// The issue id to view it + issue_id: NostrEvent, +} + +impl CommandRunner for ViewArgs { + const NEED_SIGNER: bool = false; + + async fn run(self, options: CliOptions) -> N34Result<()> { + let naddrs = utils::naddrs_or_file( + self.naddrs.flat_naddrs(&options.config.sets)?, + &utils::nostr_address_path()?, + )?; + let relays = options.relays.clone().flat_relays(&options.config.sets)?; + let client = NostrClient::init(&options, &relays).await; + + client.add_relays(&naddrs.extract_relays()).await; + client.add_relays(&self.issue_id.relays).await; + client + .add_relays( + &client + .fetch_repos(&naddrs.into_coordinates()) + .await? + .extract_relays(), + ) + .await; + + let issue = client + .fetch_event( + Filter::new() + .id(self.issue_id.event_id) + .kind(Kind::GitIssue), + ) + .await? + .ok_or(N34Error::CanNotFoundIssue)?; + + let issue_subject = utils::smart_wrap( + issue + .tags + .find(TagKind::Subject) + .and_then(|t| t.content()) + .unwrap_or("N/A"), + 70, + ); + let issue_author = client.get_username(issue.pubkey).await; + let mut issue_labels = utils::smart_wrap( + &issue + .tags + .filter(TagKind::single_letter(Alphabet::T, false)) + .filter_map(|t| t.content().map(|l| format!("#{l}"))) + .collect::>() + .join(", "), + 70, + ); + + if issue_labels.is_empty() { + issue_labels = "\n".to_owned(); + } else { + issue_labels = format!("{issue_labels}\n\n") + } + + println!( + "{issue_subject} - [by {issue_author}]\n{issue_labels}{}", + utils::smart_wrap(&issue.content, 80) + ); + Ok(()) + } +} diff --git a/src/error.rs b/src/error.rs index e60669c..0e59df5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -81,6 +81,10 @@ pub enum N34Error { EmptySetNaddrs(String), #[error("The set '{0}' doesn't contain any relays. Use 'sets update' to add addresses to it.")] EmptySetRelays(String), + #[error( + "Issue not found, make sure it is in the relays and make sure that the ID is an issue ID" + )] + CanNotFoundIssue, } impl N34Error { diff --git a/src/nostr_utils/utils.rs b/src/nostr_utils/utils.rs index f1ce36b..55378ed 100644 --- a/src/nostr_utils/utils.rs +++ b/src/nostr_utils/utils.rs @@ -251,3 +251,28 @@ pub fn event_reply_tag(reply_to: &EventId, relay: Option<&RelayUrl>, marker: Mar ], ) } + +/// Wraps text into lines no longer than max_width, breaking only at whitespace. +pub fn smart_wrap(text: &str, max_width: usize) -> String { + text.lines() + .map(|line| { + if !line.trim().is_empty() { + line.split(" ") + .fold((String::new(), 0), |(result, last_newline), word| { + let result_len = result.chars().count(); + if result_len == 0 { + (word.to_owned(), 0) + } else if (result_len - last_newline) + word.chars().count() > max_width { + (format!("{result}\n{word}"), result_len + 1) + } else { + (format!("{result} {word}"), last_newline) + } + }) + .0 + } else { + String::new() + } + }) + .collect::>() + .join("\n") +}