use std::any::Any;
use std::borrow::Cow;
use std::collections::HashMap;
use crossterm::event::{KeyCode, KeyEvent};
use ratatui_textarea::CursorMove;
use crate::compose::Target;
use crate::keymap::Action;
use crate::nav::Doable;
use crate::route::Route;
use crate::state::{Cmd, Overlay, State};
use crate::ui::form::Row;
use crate::ui::page::Link;
pub struct Kind {
pub compose: fn(&mut State, usize) -> Vec<Cmd>,
pub row: fn(&mut State, &Row) -> Vec<Cmd>,
pub fill: fn(&mut State, &Route, &Row, String) -> Vec<Cmd>,
pub submit: fn(&mut State) -> Vec<Cmd>,
pub follow: fn(&mut State) -> Vec<Cmd>,
pub offers: fn(&State, &mut Vec<Doable>),
}
fn here(state: &State) -> Option<&'static Kind> {
state.route()?.family().form()
}
pub const FEATURE: crate::features::Feature = crate::features::Feature {
owns: &[Action::Create],
takes: |state, action| action == Action::Comment && here(state).is_some(),
act: |state, action| match action {
Action::Comment => compose_here(state),
_ => submit(state),
},
offers,
follow,
};
#[must_use]
pub fn submit(state: &mut State) -> Vec<Cmd> {
let Some(kind) = here(state) else {
return state.just_say("That makes or saves a form, on its page");
};
if state.route().is_some_and(|r| state.forms.sent(r).sending) {
return say(state, "It's being sent".into());
}
(kind.submit)(state)
}
#[must_use]
pub fn compose_here(state: &mut State) -> Vec<Cmd> {
let Some(kind) = here(state) else {
return Vec::new();
};
let selected = match state.screen() {
crate::state::Screen::Page(p) => p.selected_link(),
crate::state::Screen::Diff(_) => None,
};
let part = match selected {
Some(Link::Form(Row::Text(i) | Row::Rename(i) | Row::Remove(i))) => *i,
_ => 0,
};
(kind.compose)(state, part)
}
#[must_use]
pub fn row(state: &mut State, row: &Row) -> Vec<Cmd> {
match (row, here(state)) {
(Row::New, _) => crate::state::apply(state, Action::NewHere),
(Row::Text(part), Some(kind)) => (kind.compose)(state, *part),
(Row::Save, Some(_)) => submit(state),
(_, Some(kind)) => (kind.row)(state, row),
(_, None) => Vec::new(),
}
}
pub(crate) fn filling(route: &Route, row: Row, value: impl Into<String>) -> crate::picker::Choice {
crate::picker::Choice::Step(crate::act::Step::Fill(route.clone(), row, value.into()))
}
#[must_use]
pub fn fill(state: &mut State, route: &Route, row: &Row, value: String) -> Vec<Cmd> {
let Some(kind) = route.family().form() else {
return Vec::new();
};
state.forms.sent_mut(route).error = None;
(kind.fill)(state, route, row, value)
}
#[must_use]
pub fn follow(state: &mut State) -> Vec<Cmd> {
here(state).map_or_else(Vec::new, |kind| (kind.follow)(state))
}
fn offers(state: &State, out: &mut Vec<Doable>) {
if let Some(kind) = here(state) {
(kind.offers)(state, out);
}
}
pub(crate) fn offer(
state: &State,
write: Option<&str>,
save: (&str, &'static str),
why: Option<String>,
out: &mut Vec<Doable>,
) {
let sending = state.route().is_some_and(|r| state.forms.sent(r).sending);
let busy = sending.then(|| "it's being sent".to_owned());
if let Some(label) = write {
let row = crate::nav::row("This page", Action::Comment, label, "write", busy.clone());
out.push(row);
}
let row = crate::nav::row("This page", Action::Create, save.0, save.1, busy.or(why));
out.push(row);
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Sent {
pub sending: bool,
pub error: Option<String>,
}
impl Sent {
const NONE: Sent = Sent {
sending: false,
error: None,
};
pub fn start(&mut self) {
*self = Sent {
sending: true,
error: None,
};
}
pub fn fail(&mut self, why: String) {
*self = Sent {
sending: false,
error: Some(why),
};
}
}
pub trait Fields: Any + Send {
fn text(&mut self, _part: usize) -> Option<&mut String> {
None
}
fn title(&self, _route: &Route, _part: usize) -> String {
String::new()
}
fn save(&self, _route: &Route) -> &'static str {
"save"
}
}
impl Fields for () {}
struct Entry {
version: u64,
sent: Sent,
fields: Box<dyn Fields>,
}
#[derive(Default)]
pub struct Forms {
map: HashMap<Route, Entry>,
stamp: u64,
}
impl Forms {
pub fn get<F: Fields>(&self, route: &Route) -> Option<&F> {
let fields: &dyn Any = self.map.get(route)?.fields.as_ref();
fields.downcast_ref()
}
pub fn shown<F: Fields + Clone + Default>(&self, route: &Route) -> Cow<'_, F> {
self.get(route)
.map_or_else(|| Cow::Owned(F::default()), Cow::Borrowed)
}
fn entry(&mut self, route: &Route) -> &mut Entry {
self.stamp += 1;
let entry = (self.map.entry(route.clone())).or_insert_with(|| Entry {
version: 0,
sent: Sent::default(),
fields: Box::new(()),
});
entry.version = self.stamp;
entry
}
#[expect(
clippy::expect_used,
reason = "the entry holds an F: it was made one just before"
)]
pub fn edit<F: Fields + Default>(&mut self, route: &Route) -> &mut F {
let entry = self.entry(route);
if !(entry.fields.as_ref() as &dyn Any).is::<F>() {
entry.fields = Box::new(F::default());
}
let fields: &mut dyn Any = entry.fields.as_mut();
fields.downcast_mut().expect("an F")
}
pub fn get_mut<F: Fields>(&mut self, route: &Route) -> Option<&mut F> {
self.get::<F>(route)?;
let fields: &mut dyn Any = self.entry(route).fields.as_mut();
fields.downcast_mut()
}
fn fields(&self, route: &Route) -> Option<&dyn Fields> {
Some(self.map.get(route)?.fields.as_ref())
}
fn fields_mut(&mut self, route: &Route) -> Option<&mut dyn Fields> {
self.map.get(route)?;
Some(self.entry(route).fields.as_mut())
}
pub fn take<F: Fields + Default>(&mut self, route: &Route) -> F {
let fields = self.map.remove(route).map(|e| e.fields as Box<dyn Any>);
(fields.and_then(|f| f.downcast().ok())).map_or_else(F::default, |f| *f)
}
pub fn sent(&self, route: &Route) -> &Sent {
self.map.get(route).map_or(&Sent::NONE, |e| &e.sent)
}
pub fn sent_mut(&mut self, route: &Route) -> &mut Sent {
&mut self.entry(route).sent
}
pub fn version(&self, route: &Route) -> u64 {
self.map.get(route).map_or(0, |e| e.version)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Text {
pub route: Route,
pub part: usize,
}
impl Text {
pub fn title(&self, state: &State) -> String {
let fields = state.forms.fields(&self.route);
fields
.map(|f| f.title(&self.route, self.part))
.unwrap_or_default()
}
pub fn save(&self, state: &State) -> &'static str {
let fields = state.forms.fields(&self.route);
fields.map_or("save", |f| f.save(&self.route))
}
pub fn keep(&self, state: &mut State, written: String) {
let forms = &mut state.forms;
if let Some(text) = forms
.fields_mut(&self.route)
.and_then(|f| f.text(self.part))
{
*text = written;
}
forms.sent_mut(&self.route).error = None;
}
#[must_use]
pub fn save_form(&self, state: &mut State, written: String) -> Vec<Cmd> {
self.keep(state, written);
if state.route() != Some(&self.route) {
return Vec::new();
}
crate::state::apply(state, Action::Create)
}
}
pub(crate) const TITLE: &[CursorMove] = &[CursorMove::Top, CursorMove::End];
#[must_use]
pub(crate) fn compose<F: Fields + Default>(
state: &mut State,
route: &Route,
part: usize,
cursor: &[CursorMove],
) -> Vec<Cmd> {
if state.forms.sent(route).sending {
return state.just_say("It's being sent");
}
let Some(text) = state.forms.edit::<F>(route).text(part).cloned() else {
return Vec::new();
};
let target = Target::Form(Text {
route: route.clone(),
part,
});
let cmds = state.compose(target, &text);
if let Some(Overlay::Compose(c)) = &mut state.overlay {
for &to in cursor {
c.input.move_cursor(to);
}
}
cmds
}
pub(crate) fn composing(state: &State, route: &Route, part: usize) -> bool {
matches!(&state.overlay, Some(Overlay::Compose(c))
if matches!(&c.target, Target::Form(t) if t.route == *route && t.part == part))
}
impl Overlay {
pub fn sent_mut(&mut self) -> Option<&mut Sent> {
match self {
Overlay::Compose(c) => Some(&mut c.sent),
Overlay::Ask(a) => Some(&mut a.sent),
Overlay::Submit(d) => Some(&mut d.sent),
Overlay::Find(_)
| Overlay::Search(_)
| Overlay::Hints(_)
| Overlay::Menu(_)
| Overlay::Picker(_) => None,
}
}
}
#[must_use]
pub(crate) fn say(state: &mut State, text: String) -> Vec<Cmd> {
match state.overlay.as_mut().and_then(Overlay::sent_mut) {
Some(sent) => sent.fail(text),
None => state.error(text),
}
Vec::new()
}
pub(crate) fn sending(state: &mut State) {
if let Some(sent) = state.overlay.as_mut().and_then(Overlay::sent_mut) {
sent.start();
}
}
pub(crate) fn form_sending(state: &mut State, route: &Route) {
state.forms.sent_mut(route).start();
sending(state);
}
#[must_use]
pub(crate) fn failed(state: &mut State, route: &Route, why: String) -> Vec<Cmd> {
if state.forms.map.contains_key(route) {
state.forms.sent_mut(route).fail(why.clone());
}
say(state, why)
}
pub(crate) fn close_sender(state: &mut State, route: &Route) {
let ours = match &state.overlay {
Some(Overlay::Compose(c)) => matches!(&c.target, Target::Form(t) if t.route == *route),
Some(Overlay::Ask(a)) => matches!(&a.about, About::Form(r) if r == route),
_ => false,
};
if ours {
state.overlay = None;
}
}
pub(crate) fn done<F: Fields + Default>(state: &mut State, route: &Route) -> F {
close_sender(state, route);
state.forms.take(route)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ask {
pub about: About,
pub title: String,
pub facts: Vec<(String, bool)>,
pub choices: Vec<(String, Answer)>,
pub selected: usize,
pub sent: Sent,
pub back: Option<(Target, String)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum About {
Form(Route),
Act(Action, crate::act::Subject),
}
impl Ask {
pub fn new(
about: About,
title: String,
facts: Vec<(String, bool)>,
choices: Vec<(String, Answer)>,
) -> Self {
Self {
about,
title,
facts,
choices,
selected: 0,
sent: Sent::default(),
back: None,
}
}
pub fn busy(&self) -> Option<String> {
let (_, answer) = self
.choices
.get(self.selected)
.filter(|_| self.sent.sending)?;
Some(answer.busy())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Answer {
Push,
CreatePr {
draft: bool,
},
Release {
draft: bool,
},
DeleteRelease,
DeleteTag,
KeepTag,
DeleteAsset(String),
Gist {
public: bool,
},
DeleteGist,
Site(crate::site::Answer),
Housekeeping(crate::housekeeping::Answer),
Change(crate::api::change::Change),
}
impl Answer {
pub fn busy(&self) -> String {
let busy = match self {
Answer::Change(change) => return format!("{}…", crate::act::doing(change)),
Answer::Push => "Pushing…",
Answer::CreatePr { .. } => "Creating the pull request…",
Answer::Release { draft: true } => "Saving the draft…",
Answer::Release { .. } => "Saving the release…",
Answer::DeleteRelease => "Deleting the release…",
Answer::DeleteTag => "Deleting the tag…",
Answer::KeepTag => "",
Answer::DeleteAsset(_) => "Deleting the asset…",
Answer::Gist { .. } => "Creating the gist…",
Answer::DeleteGist => "Deleting the gist…",
Answer::Site(a) => a.busy(),
Answer::Housekeeping(a) => a.busy(),
};
busy.to_owned()
}
}
pub(crate) fn ask(
state: &mut State,
route: Route,
title: String,
facts: Vec<(String, bool)>,
choices: Vec<(String, Answer)>,
) {
let ask = Ask::new(About::Form(route), title, facts, choices);
state.overlay = Some(Overlay::Ask(Box::new(ask)));
}
#[must_use]
pub fn on_ask_key(state: &mut State, key: KeyEvent) -> Vec<Cmd> {
let Some(Overlay::Ask(ask)) = &mut state.overlay else {
return Vec::new();
};
let n = ask.choices.len().max(1);
match key.code {
KeyCode::Esc => state.overlay = None,
_ if ask.sent.sending => {}
KeyCode::Tab | KeyCode::Right | KeyCode::Char('l') => ask.selected = (ask.selected + 1) % n,
KeyCode::BackTab | KeyCode::Left | KeyCode::Char('h') => {
ask.selected = (ask.selected + n - 1) % n;
}
KeyCode::Enter => {
let Some((_, answer)) = ask.choices.get(ask.selected).cloned() else {
return Vec::new();
};
return match (ask.about.clone(), answer) {
(About::Act(_, about), Answer::Change(change)) => {
crate::act::confirmed(state, change, about)
}
(About::Form(route), answer) => answered(state, &route, answer),
(About::Act(..), _) => Vec::new(),
};
}
_ => {}
}
Vec::new()
}
#[must_use]
fn answered(state: &mut State, route: &Route, answer: Answer) -> Vec<Cmd> {
match answer {
Answer::Push => crate::create::answer(state, route, None),
Answer::CreatePr { draft } => crate::create::answer(state, route, Some(draft)),
Answer::Release { draft } => crate::release::create(state, route, draft),
Answer::DeleteRelease => crate::release::delete(state, route),
Answer::DeleteTag => crate::release::delete_tag(state, route),
Answer::KeepTag => {
close_sender(state, route);
if let Route::Releases(crate::route::releases::Release { tag, .. }) = route {
state.info(format!("Kept the tag {tag}"));
}
Vec::new()
}
Answer::DeleteAsset(name) => crate::release::assets::delete(state, &name),
Answer::Gist { public } => crate::gist::create(state, route, public),
Answer::DeleteGist => crate::gist::delete(state, route),
Answer::Site(a) => crate::site::answer(state, route, a),
Answer::Housekeeping(a) => crate::housekeeping::answer(state, route, a),
Answer::Change(_) => Vec::new(),
}
}