feat: A --quote-to flag to quote the replied to content in the editor
Signed-off-by: Awiteb <a@4rs.nl>
This commit is contained in:
@@ -14,6 +14,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Read the `nostr-address` file in `repo view` command - by Awiteb
|
- Read the `nostr-address` file in `repo view` command - by Awiteb
|
||||||
- Read the `nostr-address` file in `issue new` command - by Awiteb
|
- Read the `nostr-address` file in `issue new` command - by Awiteb
|
||||||
- Read the `nostr-address` file in `reply` command - by Awiteb
|
- Read the `nostr-address` file in `reply` command - by Awiteb
|
||||||
|
- A `--quote-to` flag to quote the replied to content in the editor - by Awiteb
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- Add `chrono@0.4.41` to the dependencies - by Awiteb
|
||||||
|
|
||||||
## [0.1.0] - 2025-05-21
|
## [0.1.0] - 2025-05-21
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ impl NewArgs {
|
|||||||
return Ok((None, content.trim().to_owned()));
|
return Ok((None, content.trim().to_owned()));
|
||||||
}
|
}
|
||||||
// If the `self.content` is `None` then the `self.editor` is `true`
|
// If the `self.content` is `None` then the `self.editor` is `true`
|
||||||
let file_content = utils::read_editor(".md")?;
|
let file_content = utils::read_editor(None, ".md")?;
|
||||||
if file_content.contains('\n') {
|
if file_content.contains('\n') {
|
||||||
Ok(file_content
|
Ok(file_content
|
||||||
.split_once('\n')
|
.split_once('\n')
|
||||||
|
|||||||
@@ -18,9 +18,12 @@ use std::{fs, str::FromStr};
|
|||||||
|
|
||||||
use clap::{ArgGroup, Args};
|
use clap::{ArgGroup, Args};
|
||||||
use nostr::{
|
use nostr::{
|
||||||
event::{EventBuilder, EventId, Kind, Tag},
|
event::{Event, EventBuilder, EventId, Kind, Tag},
|
||||||
filter::Filter,
|
filter::Filter,
|
||||||
nips::nip19::{self, FromBech32, Nip19Coordinate},
|
nips::{
|
||||||
|
nip01::Metadata,
|
||||||
|
nip19::{self, FromBech32, Nip19Coordinate, ToBech32},
|
||||||
|
},
|
||||||
types::RelayUrl,
|
types::RelayUrl,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -31,6 +34,11 @@ use crate::{
|
|||||||
nostr_utils::{NostrClient, utils},
|
nostr_utils::{NostrClient, utils},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Length of a Nostr npub (public key) in characters.
|
||||||
|
const NPUB_LEN: usize = 63;
|
||||||
|
/// The max date "9999-01-01 at 00:00 UTC"
|
||||||
|
const MAX_DATE: i64 = 253370764800;
|
||||||
|
|
||||||
/// Parses and represents a Nostr `nevent1` or `note1`.
|
/// Parses and represents a Nostr `nevent1` or `note1`.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct NostrEvent {
|
struct NostrEvent {
|
||||||
@@ -74,24 +82,31 @@ impl FromStr for NostrEvent {
|
|||||||
ArgGroup::new("comment-content")
|
ArgGroup::new("comment-content")
|
||||||
.args(["comment", "editor"])
|
.args(["comment", "editor"])
|
||||||
.required(true)
|
.required(true)
|
||||||
|
),
|
||||||
|
group(
|
||||||
|
ArgGroup::new("quote-reply-to")
|
||||||
|
.args(["comment", "quote_to"])
|
||||||
)
|
)
|
||||||
)]
|
)]
|
||||||
pub struct ReplyArgs {
|
pub struct ReplyArgs {
|
||||||
/// The issue, patch, or comment to reply to
|
/// The issue, patch, or comment to reply to
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
to: NostrEvent,
|
to: NostrEvent,
|
||||||
|
/// Quote the replied-to event in the editor
|
||||||
|
#[arg(long)]
|
||||||
|
quote_to: bool,
|
||||||
/// Repository address in `naddr` format.
|
/// Repository address in `naddr` format.
|
||||||
///
|
///
|
||||||
/// If not provided, `n34` will look for the `nostr-address` file and if not
|
/// If not provided, `n34` will look for the `nostr-address` file and if not
|
||||||
/// found, will get it from the root event if found.
|
/// found, will get it from the root event if found.
|
||||||
#[arg(short, long, value_parser = parsers::repo_naddr)]
|
#[arg(short, long, value_parser = parsers::repo_naddr)]
|
||||||
naddr: Option<Nip19Coordinate>,
|
naddr: Option<Nip19Coordinate>,
|
||||||
/// The comment (cannot be used with --editor)
|
/// The comment (cannot be used with --editor)
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
comment: Option<String>,
|
comment: Option<String>,
|
||||||
/// Open editor to write comment (cannot be used with --content)
|
/// Open editor to write comment (cannot be used with --content)
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
editor: bool,
|
editor: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommandRunner for ReplyArgs {
|
impl CommandRunner for ReplyArgs {
|
||||||
@@ -148,7 +163,13 @@ impl CommandRunner for ReplyArgs {
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = utils::get_content(self.comment.as_ref(), ".txt")?;
|
let quoted_content = if self.quote_to {
|
||||||
|
Some(quote_reply_to_content(&client, &reply_to).await)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let content = utils::get_content(self.comment.as_ref(), quoted_content.as_ref(), ".txt")?;
|
||||||
let content_details = client.parse_content(&content).await;
|
let content_details = client.parse_content(&content).await;
|
||||||
write_relays.extend(content_details.write_relays.clone());
|
write_relays.extend(content_details.write_relays.clone());
|
||||||
|
|
||||||
@@ -184,3 +205,44 @@ impl CommandRunner for ReplyArgs {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a quoted reply string in the format "On yyyy-mm-dd at hh:mm UTC,
|
||||||
|
/// <author> wrote:" followed by the event content. Uses display name if
|
||||||
|
/// available, otherwise falls back to a shortened npub string. Dates are
|
||||||
|
/// formatted in UTC.
|
||||||
|
async fn quote_reply_to_content(client: &NostrClient, quoted_event: &Event) -> String {
|
||||||
|
let author_name = client
|
||||||
|
.fetch_event(
|
||||||
|
Filter::new()
|
||||||
|
.kind(Kind::Metadata)
|
||||||
|
.author(quoted_event.pubkey),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.and_then(|e| Metadata::try_from(&e).ok())
|
||||||
|
.and_then(|m| m.display_name.or(m.name))
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
let pubkey = quoted_event
|
||||||
|
.pubkey
|
||||||
|
.to_bech32()
|
||||||
|
.expect("The error is `Infallible`");
|
||||||
|
format!("{}...{}", &pubkey[..8], &pubkey[NPUB_LEN - 8..])
|
||||||
|
});
|
||||||
|
|
||||||
|
let fdate = chrono::DateTime::from_timestamp(
|
||||||
|
quoted_event
|
||||||
|
.created_at
|
||||||
|
.as_u64()
|
||||||
|
.try_into()
|
||||||
|
.unwrap_or(MAX_DATE),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.map(|datetime| datetime.format("On %F at %R UTC, ").to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"{fdate}{author_name} wrote:\n> {}",
|
||||||
|
quoted_event.content.trim().replace("\n", "\n> ")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -162,13 +162,17 @@ pub fn add_read_relays(mut vector: Vec<RelayUrl>, event: Option<&Event>) -> Vec<
|
|||||||
/// Opens the user's default editor ($EDITOR) to edit a temporary file with
|
/// Opens the user's default editor ($EDITOR) to edit a temporary file with
|
||||||
/// given suffix, then reads and returns the file contents. The temporary file
|
/// given suffix, then reads and returns the file contents. The temporary file
|
||||||
/// is automatically deleted.
|
/// is automatically deleted.
|
||||||
pub fn read_editor(file_suffix: &str) -> N34Result<String> {
|
pub fn read_editor(file_pre_content: Option<&str>, file_suffix: &str) -> N34Result<String> {
|
||||||
let Ok(editor) = std::env::var("EDITOR") else {
|
let Ok(editor) = std::env::var("EDITOR") else {
|
||||||
return Err(N34Error::EditorNotFound);
|
return Err(N34Error::EditorNotFound);
|
||||||
};
|
};
|
||||||
|
|
||||||
let temp_path = tempfile::NamedTempFile::with_suffix(file_suffix)?.into_temp_path();
|
let temp_path = tempfile::NamedTempFile::with_suffix(file_suffix)?.into_temp_path();
|
||||||
|
|
||||||
|
if let Some(pre_content) = file_pre_content {
|
||||||
|
fs::write(&temp_path, pre_content)?;
|
||||||
|
}
|
||||||
|
|
||||||
// Disable the logs to not show up in a terminal text editor
|
// Disable the logs to not show up in a terminal text editor
|
||||||
crate::EDITOR_OPEN.store(true, Ordering::Relaxed);
|
crate::EDITOR_OPEN.store(true, Ordering::Relaxed);
|
||||||
let exit_status = std::process::Command::new(editor)
|
let exit_status = std::process::Command::new(editor)
|
||||||
@@ -195,14 +199,18 @@ pub fn read_editor(file_suffix: &str) -> N34Result<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the given content if it's `Option::Some` or call [`read_editor`]
|
/// Returns the given content if it's `Option::Some` or call [`read_editor`]
|
||||||
pub fn get_content<S>(content: Option<S>, file_suffix: &str) -> N34Result<String>
|
pub fn get_content(
|
||||||
where
|
content: Option<impl AsRef<str>>,
|
||||||
S: AsRef<str>,
|
quoted_content: Option<impl AsRef<str>>,
|
||||||
{
|
file_suffix: &str,
|
||||||
|
) -> N34Result<String> {
|
||||||
if let Some(content) = content {
|
if let Some(content) = content {
|
||||||
return Ok(content.as_ref().trim().to_owned());
|
return Ok(content.as_ref().trim().to_owned());
|
||||||
}
|
}
|
||||||
read_editor(file_suffix)
|
read_editor(
|
||||||
|
quoted_content.map(|s| s.as_ref().to_owned()).as_deref(),
|
||||||
|
file_suffix,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Path to the `nostr-address` file in current directory.
|
/// Path to the `nostr-address` file in current directory.
|
||||||
|
|||||||
Reference in New Issue
Block a user