// [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
}