use std::fmt::Write as _; use std::path::PathBuf; use crossterm::event::KeyCode; use crate::state::{ItemKind, PassedMocion, Stage, StackItem, Vote}; #[derive(Debug, Clone, Default)] pub enum InputMode { #[default] Normal, ShowingHelp, } #[derive(Debug, Clone)] pub enum EditorTarget { NewItem(ItemKind), EditCurrent, EditLastPassed, } pub struct App { pub stack: Vec, pub passed: Vec, pub mode: InputMode, pub status: String, pub should_quit: bool, /// Set when a key triggers an editor open; cleared and acted on by the main loop. pub open_editor: Option, pub output_path: PathBuf, pub cache_path: PathBuf, } impl App { pub fn new(output_path: PathBuf, cache_path: PathBuf) -> Self { let passed: Vec = std::fs::read(&cache_path) .map_or(Vec::new(), |d| serde_json::from_slice(&d).map_or(Vec::new(), |s| s) ); Self { stack: Vec::new(), passed, mode: InputMode::Normal, status: "Press 'm' for new moción, 'q' to quit, '?' for help".to_string(), should_quit: false, open_editor: None, output_path, cache_path, } } pub fn handle_key(&mut self, key: KeyCode) { match self.mode { InputMode::ShowingHelp => self.mode = InputMode::Normal, InputMode::Normal => self.handle_normal_key(key), } } fn handle_normal_key(&mut self, key: KeyCode) { match key { KeyCode::Char('q') => { self.write_output(); self.should_quit = true; } KeyCode::Char('m') => { if self.stack.is_empty() { self.open_editor = Some(EditorTarget::NewItem(ItemKind::Mocion)); } else { self.status = "Resolve the current item before presenting a new moción.".to_string(); } } KeyCode::Char('e') => { if self.stack.is_empty() { self.status = "No active item to amend.".to_string(); } else { self.open_editor = Some(EditorTarget::NewItem(ItemKind::Enmienda)); } } KeyCode::Char('E') => { if self.stack.is_empty() { self.status = "No active item to substitute.".to_string(); } else { self.open_editor = Some(EditorTarget::NewItem(ItemKind::EnmiendaSustitutiva)); } } KeyCode::Char('r') => { if self.stack.is_empty() { self.status = "Nothing on the stack to edit.".to_string(); } else { self.open_editor = Some(EditorTarget::EditCurrent); } } KeyCode::Char('R') => { if self.passed.is_empty() { self.status = "No passed mociones to edit.".to_string(); } else { self.open_editor = Some(EditorTarget::EditLastPassed); } } KeyCode::Char('s') => self.secundar(), KeyCode::Char('n') => self.no_secundar("Not secundada — item removed."), KeyCode::Char('o') => self.oppose(), KeyCode::Char('O') => self.no_opposition(), KeyCode::Char('f') => self.vote_favor(), KeyCode::Char('c') => self.vote_contra(), KeyCode::Char('v') => self.finalize_vote(), KeyCode::Char('p') => self.manual_pass(), KeyCode::Char('x') => self.no_secundar("Item skipped."), KeyCode::Char('w') => self.write_output(), KeyCode::Char('?') => self.mode = InputMode::ShowingHelp, _ => {} } } /// Called by the main loop after the editor process exits with saved text. pub fn apply_editor_result(&mut self, target: EditorTarget, text: String) { if text.is_empty() { self.status = "No changes (empty text).".to_string(); return; } match target { EditorTarget::NewItem(kind) => self.push_item(kind, text), EditorTarget::EditCurrent => { if let Some(item) = self.stack.last_mut() { item.text = text; self.status = "Item text updated from editor.".to_string(); } } EditorTarget::EditLastPassed => { if let Some(m) = self.passed.last_mut() { m.text = text; self.status = format!("Moción #{} text corrected.", m.number); } } } } fn push_item(&mut self, kind: ItemKind, text: String) { let label = kind.label().to_string(); self.stack.push(StackItem::new(kind, text)); self.status = format!("{label} presented. Press 's' to secundar, 'n' if not secundada."); } fn secundar(&mut self) { let Some(item) = self.stack.last_mut() else { self.status = "Nothing on the stack.".to_string(); return; }; match item.stage { Stage::AwaitingSecundar => { item.stage = Stage::AwaitingOpposition; let label = item.kind.label().to_string(); self.status = format!("{label} secundada. Press 'o' if there is opposition, 'O' if not."); } _ => self.status = "Not awaiting secundar.".to_string(), } } fn no_secundar(&mut self, msg: &str) { if self.stack.pop().is_some() { self.status = msg.to_string(); } else { self.status = "Nothing on the stack.".to_string(); } } fn oppose(&mut self) { let Some(item) = self.stack.last_mut() else { self.status = "Nothing on the stack.".to_string(); return; }; match item.stage { Stage::AwaitingOpposition => { item.stage = Stage::Voting(Vote::default()); self.status = "Voting open. 'f' favor 'c' contra 'v' finalize 'p' force pass 'x' skip".to_string(); } _ => self.status = "Not awaiting opposition.".to_string(), } } fn no_opposition(&mut self) { let Some(item) = self.stack.last() else { self.status = "Nothing on the stack.".to_string(); return; }; match item.stage { Stage::AwaitingOpposition => self.resolve_pass(), _ => self.status = "Not awaiting opposition.".to_string(), } } fn vote_favor(&mut self) { if let Some(item) = self.stack.last_mut() { if let Stage::Voting(ref mut v) = item.stage { v.favor += 1; } } self.refresh_vote_status(); } fn vote_contra(&mut self) { if let Some(item) = self.stack.last_mut() { if let Stage::Voting(ref mut v) = item.stage { v.contra += 1; } } self.refresh_vote_status(); } fn refresh_vote_status(&mut self) { if let Some(Stage::Voting(ref v)) = self.stack.last().map(|i| &i.stage) { self.status = format!( "Voting: {} favor, {} contra — 'f' +favor 'c' +contra 'v' finalize", v.favor, v.contra, ); } } fn finalize_vote(&mut self) { let passes = if let Some(item) = self.stack.last() { if let Stage::Voting(v) = &item.stage { v.favor > v.contra } else { self.status = "Not in voting stage.".to_string(); return; } } else { self.status = "Nothing on the stack.".to_string(); return; }; if passes { self.resolve_pass(); } else { self.stack.pop(); self.status = "Vote failed — item removed.".to_string(); } } fn manual_pass(&mut self) { if self.stack.is_empty() { self.status = "Nothing on the stack.".to_string(); } else { self.resolve_pass(); } } /// Resolves the top of the stack as passed. /// /// - Moción: added to the passed list. /// - Enmienda / Enm. Sust.: replaces the text of the item below. If there is /// nothing below (e.g. the original moción was skipped), it is promoted to /// a moción and added to the passed list. fn resolve_pass(&mut self) { let Some(item) = self.stack.pop() else { return; }; match item.kind { ItemKind::Mocion => { let number = self.passed.len() + 1; self.status = format!("Moción #{number} passed!"); self.passed.push(PassedMocion { number, text: item.text }); } kind => { let label = kind.label().to_string(); if let Some(below) = self.stack.last_mut() { below.text = item.text; below.stage = Stage::AwaitingOpposition; self.status = format!("{label} passed — item text updated, returning to opposition check."); } else { let number = self.passed.len() + 1; self.status = format!("{label} promoted to moción #{number} (no target below)."); self.passed.push(PassedMocion { number, text: item.text }); } } } let serialized: String = serde_json::to_string(&self.passed).expect("json_serde will always be able to serialize this"); let _ = std::fs::write(&self.cache_path, serialized); } fn write_output(&mut self) { if self.passed.is_empty() { self.status = "No passed mociones to save yet.".to_string(); return; } let mut out = String::from("# Mociones\n\n"); for m in &self.passed { let _ = write!(out, "{}. {}\n\n", m.number, m.text); } match std::fs::write(&self.output_path, out) { Ok(()) => self.status = format!( "Saved {} moción(es) to {}.", self.passed.len(), self.output_path.display(), ), Err(e) => self.status = format!("Error saving: {e}"), } } }