use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::api::model::Capped;
use crate::api::replies::SavedReply;
use crate::data::Picked;
use crate::state::{Cmds, Overlay, Remote, State};
use crate::data::DataKey;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Choosing {
pub query: String,
pub selected: usize,
}
struct Row<'a> {
label: String,
insert: Option<&'a str>,
}
fn rows<'a>(state: &'a State, query: &str) -> Vec<Row<'a>> {
let key = DataKey::SavedReplies;
#[expect(clippy::disallowed_methods, reason = "said as the list's row")]
let fetched = Remote::fetched(state.remote(&key), "")
.pick(<Capped<SavedReply> as Picked>::pick)
.text("your saved replies");
let note = |label: String| Row {
label,
insert: None,
};
let mut rows = match fetched {
Err(why) if why.contains("read:user") => vec![
note("Your token lacks the read:user scope:".to_owned()),
note(format!("{} adds it", crate::api::refresh("read:user"))),
],
Err(why) => vec![note(why)],
Ok(replies) if replies.is_empty() => vec![note("You have no saved replies".to_owned())],
Ok(replies) => {
let has = |text: &str| crate::picker::find_ci(text, query).is_some();
let mut found: Vec<Row> = (replies.iter())
.filter(|r| has(&r.title) || has(&r.body))
.map(|r| Row {
label: r.title.clone(),
insert: Some(&r.body),
})
.collect();
if found.is_empty() {
found.push(note(format!("No saved reply has “{query}”")));
}
if replies.left_out() > 0 {
found.push(note(format!("{} more on GitHub", replies.left_out())));
}
found
}
};
let url = crate::api::replies::settings_url();
let url = url.trim_start_matches("https://");
rows.push(note(format!("Make or change them at {url}")));
rows
}
pub fn shown(state: &State) -> Option<(Vec<String>, usize)> {
let Some(Overlay::Compose(c)) = &state.overlay else { return None };
let choosing = c.saved.as_ref()?;
let mut labels = vec![format!("Saved replies ⌕ {}", choosing.query)];
labels.extend(rows(state, &choosing.query).into_iter().map(|r| r.label));
let selected = choosing.selected.min(labels.len().saturating_sub(2)) + 1;
Some((labels, selected))
}
pub fn on_key(state: &mut State, key: KeyEvent) -> Option<Cmds> {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
let open = matches!(&state.overlay, Some(Overlay::Compose(c)) if c.saved.is_some());
if key.code == KeyCode::Char('g') && ctrl {
if let Some(Overlay::Compose(c)) = &mut state.overlay {
c.saved = match c.saved {
Some(_) => None,
None => Some(Choosing::default()),
};
}
return Some(if open {
Cmds::new()
} else {
state.ensure(DataKey::SavedReplies, true)
});
}
if !open {
return None;
}
let (query, selected) = match &state.overlay {
Some(Overlay::Compose(c)) => c.saved.as_ref().map(|s| (s.query.clone(), s.selected)),
_ => None,
}?;
let rows = rows(state, &query);
let count = rows.len();
let chosen = rows.get(selected).and_then(|r| r.insert).map(str::to_owned);
let Some(Overlay::Compose(c)) = &mut state.overlay else { return None };
let choosing = c.saved.as_mut()?;
if crate::picker::move_in_list(key, &mut choosing.selected, count) {
return Some(Cmds::new());
}
match key.code {
KeyCode::Esc => c.saved = None,
KeyCode::Backspace => {
choosing.query.pop();
choosing.selected = 0;
}
KeyCode::Char(ch) if !ctrl => {
choosing.query.push(ch);
choosing.selected = 0;
}
KeyCode::Enter => {
if let Some(text) = chosen {
c.input.insert_str(&text);
c.saved = None;
c.sent.error = None;
}
}
_ => {}
}
Some(Cmds::new())
}