// [1]: . . .-- . . . . // [2]: |\ /| |-- |\| | | // [3]: ' ' ' '-- ' ' '-' // [0]: copyright 2025 dtb // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . // I'm still something of a Rust beginner. There's plenty of room for // improvement with regards to the code. Some thoughts: // - error handling (menu(1) ***PANICS*** on a bad entry) // - general code cleanup // - memory efficiency (replace `entries` with a single String) // - option parsing (vendor a dependency? use (shudder) cargo?) // - `-i file' - read input from file // - `-p prompt` - change prompt // - `-r` - reverse numeric order // - `-x` - terminate after selection // - support giving multiple files; parse them all in order // - "Usage: {} [-rx] [-i file] [-p prompt] file" // - take presentation directives from the Attributes section // - this API (stdin and argv) is general and can be used for a TUI or GUI use std::{ env::args, fs::File, io::{self, BufRead, BufReader}, process::{Command, ExitCode}, }; #[derive(Clone)] struct Entry { text: String, attr: String, cont: String, } enum State { Ignore, Text, Attributes, Content } fn error(program_name: &str, topic: &str, error: E) -> ExitCode { eprintln!("{}: {}: {}", program_name, topic, error); ExitCode::FAILURE } fn print_entries(entries: &[Entry]) { let index_len = entries.len().to_string().len(); // trippy eprintln!(""); for (index, entry) in entries.iter().enumerate() { eprint!( "[{:>index_len$}]: {}", index + 1, entry.text, index_len = index_len ); } eprintln!("[{:>index_len$}]: Return", 0); } fn main() -> ExitCode { let argv = args().collect::>(); let mut entries: Vec = Vec::new(); let exit_on_selection: bool = false; let prompt = ">>> "; let input: Box = if argv.len() > 1 { match File::open(argv[argv.len() - 1].clone()) { Err(e) => { return error(&argv[0], &argv[argv.len() - 1], &e); } Ok(file) => Box::new(BufReader::new(file)), } } else { Box::new(io::stdin().lock()) }; let user_input: Box = if argv.len() <= 1 { match File::open("/dev/tty") { Err(e) => { return error(&argv[0], &argv[argv.len() - 1], &e); } Ok(file) => Box::new(BufReader::new(file)), } } else { Box::new(io::stdin().lock()) }; // ...Oops! I did it again. // Parser { let mut entry = Entry { text: String::default(), attr: String::default(), /* insig., but */ /* if */ cont: /* == */ String::default() /* then entry is empty */ }; let mut state = State::Ignore; for line in input.lines() { let line = line.expect(""); match state { State::Ignore => { if line.chars().next() == None { state = State::Text; } } State::Text => { entry.text = line.clone() + "\n"; state = State::Attributes; } State::Attributes => { entry.attr = line.clone() + "\n"; state = State::Content; } State::Content => match line.chars().next() { Some('\t') => { let s = &line[1..]; /* remove tab */ if entry.cont.is_empty() { entry.cont = s.to_string(); } else { entry.cont.push_str(s); } entry.cont.push('\n'); } None => { entries.push(entry.clone()); entry.cont = String::default(); state = State::Text; } _ => { return error(&argv[0], &entry.text.trim_end(), "naked line"); } }, } } if !entry.cont.is_empty() { entries.push(entry); } } // Selector print_entries(&entries); eprint!("{}", prompt); for line in user_input.lines() { let line = line.expect(""); if line != "" { match line.parse::() { Err(e) => { return error(&argv[0], &line, &e); } Ok(n) => { if n != 0 { match Command::new("sh") .arg("-c") .arg(entries[n - 1].cont.clone()) .spawn() { Err(e) => { return error(&argv[0], &line, &e); } Ok(mut c) => match c.wait() { Err(e) => { return error(&argv[0], &line, &e); } Ok(_) => { print_entries(&entries); } }, } } if n == 0 || exit_on_selection { return ExitCode::SUCCESS; } } } } eprint!("{}", prompt); } ExitCode::SUCCESS }