193 lines
6.1 KiB
Rust
193 lines
6.1 KiB
Rust
// [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 <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
// 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<E: std::fmt::Display>(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::<Vec<String>>();
|
|
let mut entries: Vec<Entry> = Vec::new();
|
|
let exit_on_selection: bool = false;
|
|
let prompt = ">>> ";
|
|
|
|
let input: Box<dyn BufRead> = 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<dyn BufRead> = 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::<usize>() {
|
|
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
|
|
}
|