use crate::nntp_client::NntpClient;
use crossterm::event::{self, Event, KeyCode};
use ratatui::{
backend::CrosstermBackend,
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
Terminal,
};
use rek2_nntp::post::Article as PostArticle;
use std::fs;
use std::io::{Write, Stdout};
use std::process::Command;
use tempfile::NamedTempFile;
fn load_signature() -> Option<String> {
let sig_path = std::env::var("SIGNATURE_FILE").unwrap_or_else(|_| {
std::env::var("HOME")
.map(|h| format!("{}/.signature", h))
.unwrap_or_else(|_| String::new())
});
if sig_path.is_empty() {
return None;
}
match fs::read_to_string(&sig_path) {
Ok(s) if !s.trim().is_empty() => Some(format!("\n-- \n{}", s)),
_ => None,
}
}
fn build_reply_template(in_reply_to: &str, original_body: &str) -> String {
let quoted = original_body
.lines()
.map(|line| format!("> {}", line))
.collect::<Vec<_>>()
.join("\n");
format!("> [in reply to {}]\n\n{}\n\n", in_reply_to, quoted)
}
pub async fn ask_reply_destination(
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
original_newsgroups: &str,
current_newsgroup: &str,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
if !original_newsgroups.contains(',') {
return Ok(Some(original_newsgroups.to_string()));
}
let mut choice = 0; let groups_list: Vec<&str> = original_newsgroups.split(',').map(|s| s.trim()).collect();
loop {
let area = terminal.size()?;
let longest_line = groups_list.iter().map(|s| s.len()).max().unwrap_or(0).max(50);
let popup_width = (longest_line + 10).min(area.width as usize - 4) as u16;
let popup_height = (groups_list.len() + 10).min(20) as u16;
let popup = Rect {
x: (area.width.saturating_sub(popup_width)) / 2,
y: (area.height.saturating_sub(popup_height)) / 2,
width: popup_width,
height: popup_height,
};
terminal.draw(|f| {
let block = Block::default()
.borders(Borders::ALL)
.title("Choose Reply Destination");
f.render_widget(block, popup);
let inner = Rect {
x: popup.x + 1,
y: popup.y + 1,
width: popup.width - 2,
height: popup.height - 2,
};
let mut lines = vec![
Line::from("This article was posted to:"),
Line::from(""),
];
for group in &groups_list {
lines.push(Line::from(vec![
Span::raw(" • "),
Span::styled(*group, Style::default().fg(Color::Cyan)),
]));
}
lines.push(Line::from(""));
lines.push(Line::from("Reply to:"));
lines.push(Line::from(""));
lines.push(Line::from(vec![
Span::raw(if choice == 0 { "[X] " } else { "[ ] " }),
Span::styled(
"All newsgroups above",
if choice == 0 {
Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::White)
},
),
]));
lines.push(Line::from(vec![
Span::raw(if choice == 1 { "[X] " } else { "[ ] " }),
Span::styled(
format!("Only: {} (current)", current_newsgroup),
if choice == 1 {
Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::White)
},
),
]));
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
"Tab/j/k to switch, Enter to confirm, Esc to cancel",
Style::default().fg(Color::DarkGray),
)));
let paragraph = Paragraph::new(lines).block(Block::default());
f.render_widget(paragraph, inner);
})?;
if let Event::Key(ev) = event::read()? {
match ev.code {
KeyCode::Tab | KeyCode::Char('j') | KeyCode::Char('k') => {
choice = 1 - choice;
}
KeyCode::Enter => {
return Ok(Some(if choice == 0 {
original_newsgroups.to_string()
} else {
current_newsgroup.to_string()
}));
}
KeyCode::Esc => {
return Ok(None); }
_ => {}
}
}
}
}
pub async fn compose_reply(
client: &mut NntpClient,
newsgroup: &str,
article_id: &str,
subject: &str,
from: String,
original_newsgroups: Option<String>,
) -> Result<Option<PostArticle>, Box<dyn std::error::Error>> {
let original_body = client
.fetch_article_body(article_id)
.await
.unwrap_or_default();
let prefill = build_reply_template(&from, &original_body);
let mut temp = NamedTempFile::new()?;
write!(temp, "{}", prefill)?;
let path = temp.path().to_owned();
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "nano".into());
Command::new(editor).arg(&path).status()?;
let body = fs::read_to_string(&path)?;
if body.trim().is_empty() || body.trim() == prefill.trim() {
return Ok(None);
}
let mut full_body = body;
if let Some(sig) = load_signature() {
full_body.push_str(&sig);
}
let article = PostArticle {
from,
newsgroups: original_newsgroups.unwrap_or_else(|| newsgroup.to_string()),
subject: format!("Re: {}", subject),
body: full_body,
message_id: None,
references: Some(article_id.to_string()),
};
Ok(Some(article))
}
pub async fn compose_post(
_client: &mut NntpClient,
newsgroup: &str,
subject: &str,
from: String,
) -> Result<Option<PostArticle>, Box<dyn std::error::Error>> {
let temp = NamedTempFile::new()?;
let path = temp.path().to_owned();
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "nano".into());
Command::new(editor).arg(&path).status()?;
let body = fs::read_to_string(&path)?;
if body.trim().is_empty() {
return Ok(None);
}
let mut full_body = body;
if let Some(sig) = load_signature() {
full_body.push_str(&sig);
}
let article = PostArticle {
from,
newsgroups: newsgroup.to_string(),
subject: subject.to_string(),
body: full_body,
message_id: None,
references: None,
};
Ok(Some(article))
}