#[allow(clippy::wildcard_imports)]
use super::*;
impl App {
pub(in crate::app) fn open_planning_modal(&mut self) {
self.planning_return = self.mode.clone();
let modal = self.planning.get_or_insert_with(PlanningModal::new);
modal.prompt = None;
modal.selected = 0;
self.mode = Mode::PlanningManagement;
}
pub(in crate::app) fn planning_rows(&self) -> (Vec<PlanningRow>, Vec<PlanningRow>) {
let root = self.project_root();
let planner = Planner::from_config(self.store.config.clone(), root);
let issues = self.store.list().unwrap_or_default();
let mut cycles = Vec::new();
for name in planner.cycle_names() {
let cycle = planner.cycle(&name);
let issues = issues
.iter()
.filter(|issue| {
issue
.cycles
.as_ref()
.is_some_and(|cs| cs.iter().any(|c| c == &name))
})
.count();
cycles.push(PlanningRow {
name,
description: cycle
.and_then(|c| c.description.clone())
.unwrap_or_default(),
date_a: cycle.and_then(|c| c.start_date.clone()).unwrap_or_default(),
date_b: Some(cycle.and_then(|c| c.due_date.clone()).unwrap_or_default()),
issues,
});
}
let mut milestones = Vec::new();
if let Ok(list) = planner.list_milestones() {
for ms in list {
let issues = issues
.iter()
.filter(|issue| {
issue
.milestones
.as_ref()
.is_some_and(|ml| ml.iter().any(|x| x == &ms.name))
})
.count();
milestones.push(PlanningRow {
name: ms.name,
description: ms.description,
date_a: ms.target_date,
date_b: None,
issues,
});
}
}
(cycles, milestones)
}
pub(in crate::app) fn planning_rows_len(&self) -> usize {
let (cycles, milestones) = self.planning_rows();
match self.planning.as_ref().map(|m| m.segment) {
Some(PlanningSegment::Cycles) => cycles.len(),
Some(PlanningSegment::Milestones) => milestones.len(),
None => 0,
}
}
pub(in crate::app) fn handle_planning_key(&mut self, key: KeyEvent) -> Result<Action> {
if self.planning.is_none() {
self.mode = self.planning_return.clone();
return Ok(Action::None);
}
if let Some(prompt) = self.planning.as_ref().and_then(|m| m.prompt.clone()) {
return self.handle_planning_confirm_key(key, prompt);
}
match key.code {
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => {
self.planning = None;
self.mode = self.planning_return.clone();
Ok(Action::None)
}
KeyCode::Up | KeyCode::Char('k') => {
let len = self.planning_rows_len();
if let Some(ref mut m) = self.planning {
m.move_selected(-1, len);
}
Ok(Action::None)
}
KeyCode::Down | KeyCode::Char('j') => {
let len = self.planning_rows_len();
if let Some(ref mut m) = self.planning {
m.move_selected(1, len);
}
Ok(Action::None)
}
KeyCode::Tab | KeyCode::BackTab => {
if let Some(ref mut m) = self.planning {
m.switch_segment();
}
Ok(Action::None)
}
KeyCode::Left | KeyCode::Char('h') | KeyCode::Right | KeyCode::Char('l') => {
if let Some(m) = &mut self.planning {
m.switch_segment();
}
Ok(Action::None)
}
KeyCode::Char('n') => {
self.planning_open_create();
Ok(Action::None)
}
KeyCode::Char('e') => self.planning_open_edit(),
KeyCode::Char('x') | KeyCode::Char('X') => self.planning_confirm_delete(),
_ => Ok(Action::None),
}
}
pub(in crate::app) fn handle_planning_confirm_key(
&mut self,
key: KeyEvent,
prompt: PlanningPrompt,
) -> Result<Action> {
let PlanningPrompt::ConfirmDelete { name, .. } = prompt;
match key.code {
KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => {
match self.planning_delete(&name) {
Ok(()) => {
self.status_msg = Some(format!("Deleted {name}"));
}
Err(e) => {
self.status_msg = Some(format!("Cannot delete {name}: {e}"));
}
}
if let Some(ref mut m) = self.planning {
m.prompt = None;
}
Ok(Action::None)
}
_ => {
if let Some(ref mut m) = self.planning {
m.prompt = None;
}
Ok(Action::None)
}
}
}
pub(in crate::app) fn planning_open_create(&mut self) {
let kind = match self.planning.as_ref().map(|m| m.segment) {
Some(PlanningSegment::Milestones) => PlanningKind::Milestone,
_ => PlanningKind::Cycle,
};
self.planning_form = Some(PlanningForm::new(kind));
self.mode = Mode::PlanningForm;
}
pub(in crate::app) fn planning_open_edit(&mut self) -> Result<Action> {
let Some(modal) = self.planning.as_ref() else {
return Ok(Action::None);
};
let root = self.project_root();
let planner = Planner::from_config(self.store.config.clone(), root);
let form = match modal.segment {
PlanningSegment::Cycles => {
let name = match planner.cycle_names().into_iter().nth(modal.selected) {
Some(name) => name,
None => return Ok(Action::None),
};
let Some(cycle) = planner.cycle(&name) else {
return Ok(Action::None);
};
PlanningForm::from_cycle(&name, cycle)
}
PlanningSegment::Milestones => {
let list = planner.list_milestones().unwrap_or_default();
let Some(ms) = list.into_iter().nth(modal.selected) else {
return Ok(Action::None);
};
PlanningForm::from_milestone(&ms)
}
};
self.planning_form = Some(form);
self.mode = Mode::PlanningForm;
Ok(Action::None)
}
pub(in crate::app) fn planning_confirm_delete(&mut self) -> Result<Action> {
let Some(modal) = self.planning.as_ref() else {
return Ok(Action::None);
};
if modal.prompt.is_some() {
return Ok(Action::None);
}
let root = self.project_root();
let planner = Planner::from_config(self.store.config.clone(), root);
let (name, referencing) = match modal.segment {
PlanningSegment::Cycles => {
let Some(name) = planner.cycle_names().into_iter().nth(modal.selected) else {
return Ok(Action::None);
};
let referencing = planner
.delete_impact(&self.store, &name)
.map(|i| i.referencing_issues.len())
.unwrap_or(0);
(name, referencing)
}
PlanningSegment::Milestones => {
let list = planner.list_milestones().unwrap_or_default();
let Some(name) = list.into_iter().nth(modal.selected).map(|m| m.name) else {
return Ok(Action::None);
};
let referencing = planner
.milestone_delete_impact(&self.store, &name)
.map(|i| i.referencing_issues.len())
.unwrap_or(0);
(name, referencing)
}
};
if let Some(ref mut m) = self.planning {
m.prompt = Some(PlanningPrompt::ConfirmDelete { name, referencing });
}
Ok(Action::None)
}
pub(in crate::app) fn planning_delete(&mut self, name: &str) -> Result<()> {
let root = self.project_root();
let mut planner = Planner::from_config(self.store.config.clone(), root.clone());
match self.planning.as_ref().map(|m| m.segment) {
Some(PlanningSegment::Cycles) => {
planner.delete_cycle(name)?;
self.reload_config_from(&root)?;
}
Some(PlanningSegment::Milestones) => {
planner.delete_milestone(name)?;
}
None => {}
}
self.refresh_corpus()?;
Ok(())
}
pub(in crate::app) fn planning_form_return(&self) -> Mode {
if self.settings.is_some() {
Mode::Settings
} else {
Mode::PlanningManagement
}
}
pub(in crate::app) fn handle_planning_form_key(&mut self, key: KeyEvent) -> Result<Action> {
if self.planning_form.is_none() {
self.mode = self.planning_form_return();
return Ok(Action::None);
}
match key.code {
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Left => {
self.planning_form = None;
self.mode = self.planning_form_return();
Ok(Action::None)
}
_ => self.handle_planning_form_edit_key(key),
}
}
pub(in crate::app) fn handle_planning_form_edit_key(
&mut self,
key: KeyEvent,
) -> Result<Action> {
if self.planning_form.is_none() {
return Ok(Action::None);
}
match key.code {
KeyCode::Enter => {
if self
.planning_form
.as_ref()
.is_some_and(|f| f.focused_is_date() && !f.focused_date_is_complete())
{
self.open_date_picker(Mode::PlanningForm);
Ok(Action::None)
} else {
self.planning_form_apply()
}
}
KeyCode::Tab | KeyCode::Down => {
if let Some(ref mut f) = self.planning_form {
f.focus_next();
}
Ok(Action::None)
}
KeyCode::BackTab | KeyCode::Up => {
if let Some(ref mut f) = self.planning_form {
f.focus_prev();
}
Ok(Action::None)
}
KeyCode::Backspace => {
if let Some(ref mut f) = self.planning_form {
f.delete_char();
}
Ok(Action::None)
}
KeyCode::Char(c) => {
if let Some(ref mut f) = self.planning_form {
f.insert_char(c);
}
Ok(Action::None)
}
KeyCode::F(2) => {
if self
.planning_form
.as_ref()
.is_some_and(|f| f.focused_is_date())
{
self.open_date_picker(Mode::PlanningForm);
}
Ok(Action::None)
}
_ => Ok(Action::None),
}
}
pub(in crate::app) fn planning_form_apply(&mut self) -> Result<Action> {
let Some(form) = self.planning_form.clone() else {
self.mode = self.planning_form_return();
return Ok(Action::None);
};
let result = self.planning_form_commit(&form);
match result {
Ok(()) => {
self.planning_form = None;
self.mode = self.planning_form_return();
Ok(Action::None)
}
Err(e) => {
self.status_msg = Some(format!("Cannot save {}: {e}", form.kind.noun()));
Ok(Action::None)
}
}
}
pub(in crate::app) fn planning_form_commit(&mut self, form: &PlanningForm) -> Result<()> {
let root = self.project_root();
let mut planner = Planner::from_config(self.store.config.clone(), root.clone());
let name = form.name().trim().to_string();
if name.is_empty() {
return Err(TixError::InvalidInput("a name is required".to_string()));
}
match form.kind {
PlanningKind::Cycle => {
let overlaps = if let Some(old) = form.editing.as_deref() {
if old != name {
planner.rename_cycle_cascade(&self.store, old, &name)?;
}
planner.set_cycle_fields(
&name,
Some(form.description()),
Some(form.date_a()),
Some(form.date_b()),
)?;
Vec::new()
} else {
planner.add_cycle(Some(name.clone()))?;
planner.set_cycle_fields(
&name,
Some(form.description()),
Some(form.date_a()),
Some(form.date_b()),
)?;
planner.overlapping_cycles(&name)
};
self.reload_config_from(&root)?;
if !overlaps.is_empty() {
self.status_msg = Some(format!(
"cycle `{name}` overlaps existing cycle(s): {}",
overlaps.join(", ")
));
}
}
PlanningKind::Milestone => {
let target = if form.date_a().trim().is_empty() {
None
} else {
Some(form.date_a())
};
if let Some(old) = form.editing.as_deref() {
if old != name {
let old_milestone = planner.milestone(old)?;
planner.rename_milestone_cascade(&self.store, &old_milestone, &name)?;
}
planner.update_milestone(&name, target, Some(form.description()))?;
} else {
planner.create_milestone_file(&name, target, Some(form.description()))?;
}
}
}
self.refresh_corpus()?;
Ok(())
}
}