use crate::services::cron_service::CronJobEntry;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CronEditField {
Name,
Cron,
Prompt,
Description,
AgentRole,
TimeoutSecs,
MaxRetries,
RetryDelay,
}
impl CronEditField {
pub fn next(self) -> Self {
match self {
Self::Name => Self::Cron,
Self::Cron => Self::Prompt,
Self::Prompt => Self::Description,
Self::Description => Self::AgentRole,
Self::AgentRole => Self::TimeoutSecs,
Self::TimeoutSecs => Self::MaxRetries,
Self::MaxRetries => Self::RetryDelay,
Self::RetryDelay => Self::Name,
}
}
pub fn prev(self) -> Self {
match self {
Self::Name => Self::RetryDelay,
Self::Cron => Self::Name,
Self::Prompt => Self::Cron,
Self::Description => Self::Prompt,
Self::AgentRole => Self::Description,
Self::TimeoutSecs => Self::AgentRole,
Self::MaxRetries => Self::TimeoutSecs,
Self::RetryDelay => Self::MaxRetries,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CronDialogMode {
List,
ConfirmDelete { job_index: usize },
EditForm {
editing_index: Option<usize>,
form: CronEditForm,
focus: CronEditField,
error: Option<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CronEditForm {
pub name: String,
pub cron: String,
pub prompt: String,
pub description: String,
pub agent_role: String,
pub timeout_secs: String,
pub max_retries: String,
pub retry_delay: String,
}
impl Default for CronEditForm {
fn default() -> Self {
Self {
name: String::new(),
cron: String::new(),
prompt: String::new(),
description: String::new(),
agent_role: String::new(),
timeout_secs: String::new(),
max_retries: String::new(),
retry_delay: String::new(),
}
}
}
impl CronEditForm {
pub fn from_entry(entry: &CronJobEntry) -> Self {
Self {
name: entry.name.clone(),
cron: entry.cron_raw.clone(),
prompt: entry.prompt.clone(),
description: entry.description.clone().unwrap_or_default(),
agent_role: entry.agent_role.clone().unwrap_or_default(),
timeout_secs: entry.timeout_secs.to_string(),
max_retries: entry.max_retries.to_string(),
retry_delay: entry.retry_delay_secs.to_string(),
}
}
pub fn field_value_mut(&mut self, field: CronEditField) -> &mut String {
match field {
CronEditField::Name => &mut self.name,
CronEditField::Cron => &mut self.cron,
CronEditField::Prompt => &mut self.prompt,
CronEditField::Description => &mut self.description,
CronEditField::AgentRole => &mut self.agent_role,
CronEditField::TimeoutSecs => &mut self.timeout_secs,
CronEditField::MaxRetries => &mut self.max_retries,
CronEditField::RetryDelay => &mut self.retry_delay,
}
}
pub fn to_entry(&self, default_timeout_secs: u64) -> Result<CronJobEntry, String> {
if self.name.trim().is_empty() {
return Err("Name is required.".to_string());
}
if self.cron.trim().is_empty() {
return Err("Cron expression is required.".to_string());
}
crate::services::cron_service::validate_cron_expr(self.cron.trim())?;
if self.prompt.trim().is_empty() {
return Err("Prompt is required.".to_string());
}
let timeout_secs = if self.timeout_secs.trim().is_empty() {
default_timeout_secs
} else {
self.timeout_secs
.trim()
.parse::<u64>()
.map_err(|_| "Timeout must be a valid number.".to_string())?
};
let max_retries = if self.max_retries.trim().is_empty() {
0
} else {
self.max_retries
.trim()
.parse::<u32>()
.map_err(|_| "Max retries must be a valid number.".to_string())?
};
let retry_delay_secs = if self.retry_delay.trim().is_empty() {
60
} else {
self.retry_delay
.trim()
.parse::<u64>()
.map_err(|_| "Retry delay must be a valid number.".to_string())?
};
Ok(CronJobEntry {
name: self.name.trim().to_string(),
description: if self.description.trim().is_empty() {
None
} else {
Some(self.description.trim().to_string())
},
cron_raw: self.cron.trim().to_string(),
cron_valid: true,
prompt: self.prompt.trim().to_string(),
agent_role: if self.agent_role.trim().is_empty() {
None
} else {
Some(self.agent_role.trim().to_string())
},
timeout_secs,
enabled: true,
max_retries,
retry_delay_secs,
})
}
}
#[derive(Debug, Clone)]
pub struct CronDialog {
pub mode: CronDialogMode,
pub selected: usize,
pub jobs: Vec<CronJobEntry>,
pub jobs_file: std::path::PathBuf,
pub default_timeout_secs: u64,
pub cron_section_present: bool,
}
impl CronDialog {
pub fn new(
jobs: Vec<CronJobEntry>,
jobs_file: std::path::PathBuf,
default_timeout_secs: u64,
cron_section_present: bool,
) -> Self {
Self {
mode: CronDialogMode::List,
selected: if jobs.is_empty() { 0 } else { 0 },
jobs,
jobs_file,
default_timeout_secs,
cron_section_present,
}
}
pub fn move_up(&mut self) {
if self.mode != CronDialogMode::List {
return;
}
self.selected = self.selected.saturating_sub(1);
}
pub fn move_down(&mut self) {
if self.mode != CronDialogMode::List {
return;
}
let max = if self.jobs.is_empty() {
0
} else {
self.jobs.len().saturating_sub(1)
};
self.selected = (self.selected + 1).min(max);
}
pub fn start_add(&mut self) {
self.mode = CronDialogMode::EditForm {
editing_index: None,
form: CronEditForm::default(),
focus: CronEditField::Name,
error: None,
};
}
pub fn start_edit(&mut self) {
if let Some(entry) = self.jobs.get(self.selected) {
let form = CronEditForm::from_entry(entry);
self.mode = CronDialogMode::EditForm {
editing_index: Some(self.selected),
form,
focus: CronEditField::Name,
error: None,
};
}
}
pub fn start_delete_confirm(&mut self) {
if self.jobs.get(self.selected).is_some() {
self.mode = CronDialogMode::ConfirmDelete {
job_index: self.selected,
};
}
}
pub fn confirm_delete(&mut self) -> anyhow::Result<()> {
if let CronDialogMode::ConfirmDelete { job_index } = self.mode {
self.jobs.remove(job_index);
self.selected = if self.jobs.is_empty() {
0
} else {
self.selected.min(self.jobs.len().saturating_sub(1))
};
self.mode = CronDialogMode::List;
self.save()?;
}
Ok(())
}
pub fn toggle_enabled(&mut self) -> anyhow::Result<()> {
if let Some(entry) = self.jobs.get_mut(self.selected) {
entry.enabled = !entry.enabled;
self.save()?;
}
Ok(())
}
pub fn save_edit_form(&mut self) -> Result<(), String> {
if let CronDialogMode::EditForm {
editing_index,
form,
..
} = &self.mode
{
let entry = form.to_entry(self.default_timeout_secs)?;
match editing_index {
Some(index) => {
let old_name = &self.jobs[*index].name;
if entry.name != *old_name && self.jobs.iter().any(|j| j.name == entry.name) {
return Err(format!("Job name '{}' already exists.", entry.name));
}
self.jobs[*index] = entry;
}
None => {
if self.jobs.iter().any(|j| j.name == entry.name) {
return Err(format!("Job name '{}' already exists.", entry.name));
}
self.jobs.push(entry);
self.selected = self.jobs.len().saturating_sub(1);
}
}
self.mode = CronDialogMode::List;
}
Ok(())
}
fn save(&self) -> anyhow::Result<()> {
let snapshot = crate::services::cron_service::CronConfigSnapshot {
jobs_file: self.jobs_file.clone(),
cron_section_present: self.cron_section_present,
default_timeout_secs: self.default_timeout_secs,
jobs: self.jobs.clone(),
};
crate::services::cron_service::write_jobs(&snapshot, &self.jobs)
}
pub fn back_to_list(&mut self) {
self.mode = CronDialogMode::List;
}
pub fn edit_next_field(&mut self) {
if let CronDialogMode::EditForm { focus, .. } = &mut self.mode {
*focus = focus.next();
}
}
pub fn edit_prev_field(&mut self) {
if let CronDialogMode::EditForm { focus, .. } = &mut self.mode {
*focus = focus.prev();
}
}
pub fn edit_push_char(&mut self, c: char) {
if let CronDialogMode::EditForm { form, focus, .. } = &mut self.mode {
form.field_value_mut(*focus).push(c);
}
}
pub fn edit_backspace(&mut self) {
if let CronDialogMode::EditForm { form, focus, .. } = &mut self.mode {
form.field_value_mut(*focus).pop();
}
}
pub fn edit_clear_error(&mut self) {
if let CronDialogMode::EditForm { error, .. } = &mut self.mode {
*error = None;
}
}
pub fn edit_set_error(&mut self, msg: String) {
if let CronDialogMode::EditForm { error, .. } = &mut self.mode {
*error = Some(msg);
}
}
}