Something went wrong. Try again.
A TUI for parliament
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146#![deny(clippy::all)]#![warn(clippy::pedantic)]#![warn(clippy::nursery)]#![warn(clippy::unwrap_used)]#![warn(clippy::panic)]#![warn(clippy::todo)]#![allow(clippy::missing_errors_doc)]
mod app;mod state;mod ui;
use std::io;use std::path::PathBuf;use std::process::Command;
use clap::Parser;use crossterm::{ event::{self, Event, KeyEventKind}, execute, terminal::{EnterAlternateScreen, LeaveAlternateScreen},};use ratatui::{Terminal, backend::CrosstermBackend};
use crate::app::{App, EditorTarget};
#[derive(Parser)]#[command(version, about = "Terminal UI for tracking parliamentary meetings")]struct Cli { /// File to write passed mociones to #[arg(short, long, default_value = "mociones.md")] output: PathBuf,
/// Editor command to use (overrides $EDITOR) #[arg(long)] editor: Option<String>,
/// File to temporarily save and load mociones from #[arg(short, long, default_value = "mociones.json")] cache: PathBuf,}
fn main() -> io::Result<()> { let cli = Cli::parse();
let editor = cli .editor .unwrap_or_else(|| std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string()));
crossterm::terminal::enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?;
let result = run(&mut terminal, cli.output, &editor, cli.cache);
let _ = crossterm::terminal::disable_raw_mode(); let _ = execute!(terminal.backend_mut(), LeaveAlternateScreen); let _ = terminal.show_cursor();
result}
fn run( terminal: &mut Terminal<CrosstermBackend<io::Stdout>>, output_path: PathBuf, editor: &str, cache_path: PathBuf) -> io::Result<()> { let mut app = App::new(output_path, cache_path);
// https://www.youtube.com/watch?v=fMayh7FhLGE loop { terminal.draw(|frame| ui::draw(frame, &app))?;
if let Event::Key(key) = event::read()? { if key.kind == KeyEventKind::Press { app.handle_key(key.code); } }
if let Some(target) = app.open_editor.take() { let initial = match &target { EditorTarget::NewItem(_) => String::new(), EditorTarget::EditCurrent => { app.stack.last().map_or_else(String::new, |i| i.text.clone()) } EditorTarget::EditLastPassed => { app.passed.last().map_or_else(String::new, |m| m.text.clone()) } EditorTarget::EnterVotes => { let (f, c) = app.current_vote_counts(); format!("# Introduce los votos (líneas con # son ignoradas)\nfavor: {f}\ncontra: {c}") } }; match open_in_editor(terminal, &initial, editor)? { Some(text) => app.apply_editor_result(target, text), None => app.status = "Editor closed without saving.".to_string(), } }
if app.should_quit { return Ok(()); } }}
/// Suspends the TUI, opens `editor` with `initial_text` pre-filled in a temp/// file, then resumes the TUI and returns the saved text./// Returns `None` if the editor exited with a non-zero status.fn open_in_editor( terminal: &mut Terminal<CrosstermBackend<io::Stdout>>, initial_text: &str, editor: &str,) -> io::Result<Option<String>> {
// file name let ts = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map_or(0, |d| d.as_millis()); let path = std::env::temp_dir().join(format!("parl_{ts}.txt")); std::fs::write(&path, initial_text)?;
crossterm::terminal::disable_raw_mode()?; execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
let exit_status = Command::new(editor).arg(&path).status();
crossterm::terminal::enable_raw_mode()?; execute!(terminal.backend_mut(), EnterAlternateScreen)?; terminal.clear()?;
match exit_status? { s if s.success() => { let text = std::fs::read_to_string(&path)?; let _ = std::fs::remove_file(&path); Ok(Some(text.trim().to_string())) } _ => { let _ = std::fs::remove_file(&path); Ok(None) } }}