use anyhow::Result;
use crossterm::event::{KeyCode, KeyModifiers};
use super::{Modal, ModalAction};
use crate::event_loop::{build_status, Buffer, LoopCtx, NewIssueDraft};
use crate::render::{Renderer, UiLine};
use crate::state::UiState;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Step {
Title,
Description,
}
pub struct IssueWizard {
owner: String,
repo: String,
step: Step,
title: String,
prompt_shown: bool,
desc_prompt_shown: bool,
}
impl IssueWizard {
pub fn open(owner: String, repo: String) -> Self {
Self {
owner,
repo,
step: Step::Title,
title: String::new(),
prompt_shown: false,
desc_prompt_shown: false,
}
}
}
impl Modal for IssueWizard {
fn handle_key(
&mut self,
code: KeyCode,
mods: KeyModifiers,
buf: &mut Buffer,
state: &mut UiState,
ctx: &mut LoopCtx,
renderer: &mut dyn Renderer,
) -> Result<ModalAction> {
match code {
KeyCode::Esc => {
push(renderer, &crate::i18n::t(crate::i18n::Msg::IssueCancelled));
buf.text.clear();
buf.cursor = 0;
Ok(ModalAction::Close)
}
KeyCode::Enter
if self.step == Step::Description
&& (mods.contains(KeyModifiers::SHIFT) || mods.contains(KeyModifiers::ALT)) =>
{
buf.text.push('\n');
buf.cursor = buf.text.len();
self.draw(buf, state, ctx, renderer);
Ok(ModalAction::Continue)
}
KeyCode::Enter => {
let entered = buf.text.trim().to_string();
if entered.is_empty() {
let what = match self.step {
Step::Title => "title",
Step::Description => "description",
};
push(
renderer,
&crate::i18n::t(crate::i18n::Msg::IssueRequiredField { field: what }),
);
return Ok(ModalAction::Continue);
}
buf.text.clear();
buf.cursor = 0;
match self.step {
Step::Title => {
self.title = entered;
self.step = Step::Description;
self.emit_description_prompt(renderer);
self.draw(buf, state, ctx, renderer);
Ok(ModalAction::Continue)
}
Step::Description => {
ctx.pending_new_issue = Some(NewIssueDraft {
owner: self.owner.clone(),
repo: self.repo.clone(),
title: std::mem::take(&mut self.title),
body: entered,
});
Ok(ModalAction::Close)
}
}
}
KeyCode::Backspace => {
if !buf.text.is_empty() {
let len = buf.text.len();
let mut end = len;
while end > 0 && !buf.text.is_char_boundary(end - 1) {
end -= 1;
}
if end > 0 {
buf.text.truncate(end - 1);
}
buf.cursor = buf.text.len();
self.draw(buf, state, ctx, renderer);
}
Ok(ModalAction::Continue)
}
KeyCode::Char(c) => {
buf.text.push(c);
buf.cursor = buf.text.len();
self.draw(buf, state, ctx, renderer);
Ok(ModalAction::Continue)
}
_ => Ok(ModalAction::Continue),
}
}
fn draw(&self, buf: &Buffer, state: &UiState, ctx: &LoopCtx, renderer: &mut dyn Renderer) {
renderer.render(UiLine::InputPrompt {
buf: buf.text.clone(),
cursor_byte: buf.cursor,
menu: None,
status: build_status(state, ctx),
attachments: Vec::new(),
});
renderer.flush();
}
}
impl IssueWizard {
pub fn emit_prompt(&mut self, renderer: &mut dyn Renderer) {
if self.prompt_shown {
return;
}
self.prompt_shown = true;
push(
renderer,
&crate::i18n::t(crate::i18n::Msg::IssueNewOn { owner: &self.owner, repo: &self.repo }),
);
push(
renderer,
&crate::i18n::t(crate::i18n::Msg::IssueStep1),
);
}
fn emit_description_prompt(&mut self, renderer: &mut dyn Renderer) {
if self.desc_prompt_shown {
return;
}
self.desc_prompt_shown = true;
push(renderer, "");
push(
renderer,
&crate::i18n::t(crate::i18n::Msg::IssueTitleConfirmed { title: &abbreviate(&self.title, 80) }),
);
push(
renderer,
&crate::i18n::t(crate::i18n::Msg::IssueStep2),
);
}
}
fn push(renderer: &mut dyn Renderer, text: &str) {
renderer.render(UiLine::CommandOutput(format!(" {}\n", text)));
renderer.flush();
}
fn abbreviate(s: &str, max: usize) -> String {
if s.chars().count() <= max {
s.to_string()
} else {
let head: String = s.chars().take(max.saturating_sub(1)).collect();
format!("{}…", head)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn open_starts_on_title_step() {
let w = IssueWizard::open("o".into(), "r".into());
assert_eq!(w.step, Step::Title);
assert!(w.title.is_empty());
assert!(!w.prompt_shown);
}
#[test]
fn abbreviate_short_string_unchanged() {
assert_eq!(abbreviate("hello", 80), "hello");
}
#[test]
fn abbreviate_long_string_tail_ellipsis() {
let long = "x".repeat(100);
let out = abbreviate(&long, 10);
assert!(out.ends_with('…'));
assert_eq!(out.chars().count(), 10);
}
}