use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui_textarea::TextArea;
use crate::act::By;
use crate::api::change::Change;
use crate::api::model::{NodeId, RepoId};
use crate::forms::{Sent, say};
use crate::review::{Comment, EditPurpose, Preview};
use crate::state::{Cmd, Overlay, State};
use crate::theme::{Bg, Theme};
use crate::ui::text::short_sha;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Target {
Review(Comment),
Conversation {
subject_id: NodeId,
name: String,
quoting: Option<String>,
},
Commit { repo: RepoId, oid: String },
Edit(crate::edit::Editing),
Discussion(crate::discuss::Writing),
NewDiscussion(crate::manage::Starting),
ProjectDraft(crate::projects::Drafting),
Setting(crate::settings::Form),
Form(crate::forms::Text),
}
impl Target {
pub fn title(&self, state: &State) -> String {
match self {
Target::Review(c) => c.title(),
Target::Conversation { name, .. } => format!("Comment on {name}"),
Target::Commit { oid, .. } => format!("Comment on commit {}", short_sha(oid)),
Target::Edit(e) => format!("Edit {}", e.name),
Target::Discussion(w) => w.title.clone(),
Target::NewDiscussion(s) => s.title(),
Target::ProjectDraft(d) => d.title.clone(),
Target::Setting(f) => f.title(),
Target::Form(text) => text.title(state),
}
}
pub fn save(&self, state: &State) -> &'static str {
match self {
Target::Review(c) => c.save(),
Target::Conversation { .. } | Target::Commit { .. } => "post comment",
Target::Edit(_) => "save",
Target::Discussion(w) => w.save(),
Target::NewDiscussion(_) => "start discussion",
Target::ProjectDraft(d) if d.draft.is_some() => "save",
Target::ProjectDraft(_) => "add draft",
Target::Setting(f) => f.save(),
Target::Form(text) => text.save(state),
}
}
}
pub struct Compose {
pub target: Target,
pub input: TextArea<'static>,
pub preview: Option<Preview>,
pub sent: Sent,
pub discard_armed: Option<&'static str>,
pub completing: crate::typing::Completing,
pub saved: Option<crate::site::replies::Choosing>,
}
impl Compose {
pub fn new(theme: &Theme, target: Target, text: &str) -> Self {
let mut input = editor(theme, text);
match target {
Target::ProjectDraft(_) if input.lines().len() == 1 => {
input.insert_newline();
input.insert_newline();
}
Target::Setting(_) | Target::NewDiscussion(_) => {
for to in crate::forms::TITLE {
input.move_cursor(*to);
}
}
_ => {}
}
Self {
target,
input,
preview: None,
sent: Sent::default(),
discard_armed: None,
completing: crate::typing::Completing::default(),
saved: None,
}
}
pub fn text(&self) -> String {
self.input.lines().join("\n")
}
pub fn note(&self) -> Option<&str> {
match &self.target {
Target::Review(c) => c.note(),
_ => None,
}
}
pub fn rows(&self) -> u16 {
let preview = (self.preview.as_ref()).map(|p| p.original.len() + p.suggested.len());
crate::ui::review_sheets::compose_rows(preview, self.note().is_some())
}
}
pub(crate) fn editor(theme: &Theme, text: &str) -> TextArea<'static> {
let mut input = crate::nav::new_input(theme, Bg::ContainerHigh);
input.set_wrap_mode(ratatui_textarea::WrapMode::WordOrGlyph);
input.insert_str(text.trim_end_matches('\n'));
input
}
#[must_use]
pub(crate) fn on_key(state: &mut State, key: KeyEvent) -> Vec<Cmd> {
let Some(Overlay::Compose(compose)) = &mut state.overlay else {
return Vec::new();
};
if compose.sent.sending {
return Vec::new();
}
if let Some(cmds) = crate::site::replies::on_key(state, key) {
return cmds;
}
if let Some(cmds) = crate::typing::on_compose_key(state, key) {
return cmds;
}
let Some(Overlay::Compose(compose)) = &mut state.overlay else {
return Vec::new();
};
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
match key.code {
KeyCode::Esc if let Target::Form(text) = &compose.target => {
let (text, written) = (text.clone(), compose.text());
text.keep(state, written);
state.overlay = None;
}
KeyCode::Esc => {
if may_discard(state, "esc", "discard") {
state.overlay = None;
}
}
KeyCode::Char('e') if ctrl => {
return vec![Cmd::Edit {
purpose: EditPurpose::Compose,
text: compose.text(),
}];
}
KeyCode::Char('s') if ctrl => return save(state),
_ => {
compose.discard_armed = None;
compose.sent.error = None;
compose.completing.selected = 0;
compose.input.input(key);
}
}
Vec::new()
}
pub(crate) fn may_discard(state: &mut State, key: &'static str, verb: &str) -> bool {
let (text, armed, error, what) = match &mut state.overlay {
Some(Overlay::Compose(c)) => (c.text(), &mut c.discard_armed, &mut c.sent.error, "comment"),
Some(Overlay::Submit(d)) => (
d.input.lines().join("\n"),
&mut d.discard_armed,
&mut d.sent.error,
"summary",
),
_ => return true,
};
if text.trim().is_empty() || *armed == Some(key) {
return true;
}
*armed = Some(key);
*error = Some(format!("Press {key} again to {verb} this {what}"));
false
}
#[must_use]
pub(crate) fn save(state: &mut State) -> Vec<Cmd> {
let Some(Overlay::Compose(compose)) = &state.overlay else {
return Vec::new();
};
let (target, body) = (compose.target.clone(), compose.text());
match target {
Target::Form(text) => text.save_form(state, body),
_ if body.trim().is_empty() => say(state, "Write something first".into()),
Target::Review(c) => crate::review::save(state, c, body),
Target::Conversation { subject_id, .. } => {
let change = Change::Comment {
subject: subject_id,
body,
};
posted(state, change, By::Compose)
}
Target::Commit { repo, oid } => posted(
state,
Change::CommitComment { repo, oid, body },
By::Compose,
),
Target::Edit(e) => {
let edit = crate::api::edit::Edit::Body {
of: e.of,
id: e.id,
body,
};
posted(state, Change::Edit(edit), By::Edit(e.about))
}
Target::Discussion(w) => {
crate::forms::sending(state);
crate::discuss::send(&w, body)
}
Target::NewDiscussion(s) => crate::manage::start(state, &s, &body),
Target::ProjectDraft(d) => match d.send(&body) {
Ok(cmds) => {
crate::forms::sending(state);
cmds
}
Err(why) => say(state, why),
},
Target::Setting(f) => crate::settings::submit(state, &f, &body),
}
}
fn posted(state: &mut State, change: Change, by: By) -> Vec<Cmd> {
crate::forms::sending(state);
vec![crate::act::send(change, by)]
}