Something went wrong. Try again.
A TUI for parliament
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298use 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<StackItem>, pub passed: Vec<PassedMocion>, 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<EditorTarget>, pub output_path: PathBuf, pub cache_path: PathBuf,}
impl App { pub fn new(output_path: PathBuf, cache_path: PathBuf) -> Self { let passed: Vec<PassedMocion> = 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}"), } }}