use std::fmt::Write as _;
use std::path::Path;
use anyhow::Result;
use config::Config;
use ipc::{BrokerRequest, BrokerResponse, MessageDto};
use session::live::models::LiveMessage;
use session::live::ListFilter;
use store::{MessageRow, Store, STATUS_UNREAD};
use transport::iroh::Endpoint;
use crate::cli::{folder_of, FolderArg};
use crate::commands::{broker, conn, live_footer};
pub(crate) struct ListOpts {
pub folder: Option<FolderArg>,
pub unread: bool,
pub from: Option<String>,
pub since: Option<String>,
pub limit: Option<u16>,
pub offset: Option<u16>,
pub long: bool,
}
pub(crate) async fn run(
cfg: &Config,
endpoint: Option<&Endpoint>,
device: Option<&str>,
opts: ListOpts,
config_path: Option<&Path>,
) -> Result<String> {
let folder = folder_of(opts.folder);
if endpoint.is_none() {
let rows = list_via_broker(cfg, device, config_path, &opts, folder.as_str()).await?;
let lines: Vec<MsgLine> = rows.iter().map(MsgLine::from_dto).collect();
return Ok(live_footer(render_rows(&lines, opts.long)));
}
let mut client = conn::connect_map(cfg, endpoint, device).await?;
let result = session::live::list(&mut client, folder, &to_filter(&opts)).await;
if let Err(e) = client.disconnect().await {
tracing::warn!("MAP disconnect failed: {e}");
}
let rows = result?;
let lines: Vec<MsgLine> = rows.iter().map(MsgLine::from_live).collect();
Ok(live_footer(render_rows(&lines, opts.long)))
}
pub(crate) async fn run_store(opts: ListOpts, store: &Store) -> Result<String> {
let folder_str = folder_of(opts.folder).as_str();
let since_ms = opts.since.as_deref().and_then(session::sync::datetime_to_ms);
let rows = store
.list_messages(
Some(folder_str),
opts.unread,
opts.from.as_deref(),
since_ms,
opts.limit.unwrap_or(1024),
opts.offset.unwrap_or(0),
)
.await?;
let lines: Vec<MsgLine> = rows.iter().map(MsgLine::from_row).collect();
let mut out = render_rows(&lines, opts.long);
if !out.ends_with('\n') {
out.push('\n');
}
out.push_str(&crate::commands::freshness_line(store.latest_sync_at().await?));
Ok(out)
}
async fn list_via_broker(
cfg: &Config,
device: Option<&str>,
config_path: Option<&Path>,
opts: &ListOpts,
folder: &str,
) -> Result<Vec<MessageDto>> {
let req = BrokerRequest::ListMessages {
folder: Some(folder.to_owned()),
unread: opts.unread,
from: opts.from.clone(),
since: opts.since.clone(),
limit: opts.limit,
offset: opts.offset.unwrap_or(0),
};
match broker::call(cfg, device, config_path, req).await? {
BrokerResponse::Messages(rows) => Ok(rows),
BrokerResponse::Failed(reason) => Err(anyhow::anyhow!("{reason}")),
BrokerResponse::Error(e) => Err(anyhow::anyhow!("{e}")),
other => Err(anyhow::anyhow!("unexpected broker response: {other:?}")),
}
}
fn to_filter(opts: &ListOpts) -> ListFilter {
ListFilter {
unread: opts.unread,
from: opts.from.clone(),
since_ms: opts.since.as_deref().and_then(session::sync::datetime_to_ms),
limit: opts.limit,
offset: opts.offset.unwrap_or(0),
}
}
struct MsgLine<'a> {
unread: bool,
timestamp_ms: i64,
address: &'a str,
text: &'a str,
handle: &'a str,
badge: &'static str,
}
impl<'a> MsgLine<'a> {
fn from_row(row: &'a MessageRow) -> Self {
Self {
unread: row.status == STATUS_UNREAD,
timestamp_ms: row.timestamp_ms,
address: &row.address,
text: &row.text,
handle: &row.map_handle,
badge: outgoing_badge(row.outgoing_status.as_ref()),
}
}
fn from_dto(m: &'a MessageDto) -> Self {
Self {
unread: !m.read,
timestamp_ms: m.timestamp_ms,
address: &m.address,
text: &m.text,
handle: &m.handle,
badge: "",
}
}
fn from_live(m: &'a LiveMessage) -> Self {
Self {
unread: !m.read,
timestamp_ms: m.timestamp_ms,
address: &m.address,
text: &m.text,
handle: &m.handle,
badge: "",
}
}
}
fn render_rows(lines: &[MsgLine], long: bool) -> String {
if lines.is_empty() {
return "(no messages)".to_owned();
}
let cap = lines.len().saturating_mul(if long { 120 } else { 96 });
let mut out = String::with_capacity(cap);
for line in lines {
let flag = if line.unread { '*' } else { ' ' };
let dt = session::sync::ms_to_display(line.timestamp_ms);
let preview: String = line.text.chars().take(72).collect();
if long {
let _ = writeln!(
out,
"{flag} {} {} {} {}{}",
line.handle, dt, line.address, preview, line.badge,
);
} else {
let _ = writeln!(out, "{flag} {} {} {}{}", dt, line.address, preview, line.badge);
}
}
out
}
const fn outgoing_badge(status: Option<&store::OutgoingStatus>) -> &'static str {
use store::OutgoingStatus;
match status {
None => "",
Some(OutgoingStatus::Queued) => " [queued]",
Some(OutgoingStatus::Sending) => " [sending]",
Some(OutgoingStatus::SentUnconfirmed) => " [sent?]",
Some(OutgoingStatus::SentConfirmed) => " [confirmed]",
Some(OutgoingStatus::FailedRetryable) => " [failed: retryable]",
Some(OutgoingStatus::FailedPermanent) => " [failed]",
Some(OutgoingStatus::Unknown) => " [unknown]",
}
}