From da96985a407ed5db831f5dd43b030c58db570ebd Mon Sep 17 00:00:00 2001
From: Awiteb
Date: Mon, 9 Jun 2025 22:06:56 +0000
Subject: [PATCH] feat: New `issue view` command to view an issue
Signed-off-by: Awiteb
---
CHANGELOG.md | 1 +
src/cli/commands/issue/mod.rs | 7 ++-
src/cli/commands/issue/view.rs | 111 +++++++++++++++++++++++++++++++++
src/error.rs | 4 ++
src/nostr_utils/utils.rs | 25 ++++++++
5 files changed, 147 insertions(+), 1 deletion(-)
create mode 100644 src/cli/commands/issue/view.rs
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")
+}