initial commit
This commit is contained in:
commit
99e3eba54a
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
.c*
|
||||
12
Cargo.toml
Normal file
12
Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "motd"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
rand = "0.8"
|
||||
clap = { version = "4.0", features = ["derive"] }
|
||||
clap_complete = "4.0"
|
||||
regex = "1.10"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
toml = "0.8"
|
||||
25
LICENSE-MIT
Normal file
25
LICENSE-MIT
Normal file
@ -0,0 +1,25 @@
|
||||
Copyright (c) Filip Bicki <candle@lampnet.io>
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
156
README.md
Normal file
156
README.md
Normal file
@ -0,0 +1,156 @@
|
||||
# MOTD Maker - motd
|
||||
|
||||
`motd` is a command line utility which outputs a (semi) intelligible, randomly generated message to your standard output.
|
||||
|
||||
## Build and Install
|
||||
##### Requirements
|
||||
`git`, `rust`/`rustup`/`cargo`
|
||||
##### Arch Linux
|
||||
`sudo pacman -S git rust`
|
||||
##### Ubuntu/Debian
|
||||
`sudo apt install git rust`
|
||||
|
||||
#### Install
|
||||
```bash
|
||||
git clone https://code.lampnet.io/candle/motdmaker
|
||||
cd motdmaker && cargo build --release
|
||||
sudo mv target/release/motd /usr/local/bin/
|
||||
```
|
||||
You may remove the cloned directory.
|
||||
|
||||
## Usage
|
||||
|
||||
`motd` is best when customized with your own word pools and templates.
|
||||
|
||||
`motd` will look for a `$HOME/.motdrc`, `$XDG_CONFIG_HOME/.motdrc`, or `$XDG_CONFIG_HOME/motd/motdrc` file and use the settings there as defaults when running `motd` with no flags. If somehow `motd` is configured in a way that would result in an empty word list, it will fallback to the built in defaults.
|
||||
|
||||
```
|
||||
# Example motdrc
|
||||
# This file can be placed at:
|
||||
# ~/.motdrc
|
||||
# $XDG_CONFIG_HOME/.motdrc
|
||||
# $XDG_CONFIG_HOME/motd/motdrc
|
||||
# (or ~/.config/motd/motdrc if XDG_CONFIG_HOME is not set)
|
||||
|
||||
# Word lists - add custom words to the defaults
|
||||
adjective = ["mystical", "ancient", "shimmering", "ethereal"]
|
||||
noun = ["artifact", "prophecy", "wanderer", "sanctuary"]
|
||||
verb = ["whispers", "echoes", "illuminates", "beckons"]
|
||||
adverb = ["mysteriously", "gracefully", "eternally", "silently"]
|
||||
location = ["in the forgotten realm", "beneath the starlit sky", "within the crystal caves"]
|
||||
|
||||
# Template customization
|
||||
# template = "The {adjective} {noun} {verb} {adverb} {location}."
|
||||
|
||||
# Behavior settings
|
||||
# Set to true to replace default word lists instead of extending them
|
||||
replace = false
|
||||
# Convert output to lowercase
|
||||
lowercase = false
|
||||
# Convert output to uppercase
|
||||
uppercase = false
|
||||
|
||||
# Text processing
|
||||
# Words to remove from output
|
||||
strip = [".", ":"]
|
||||
# Delimiter between words (default is space)
|
||||
delimiter = "."
|
||||
# Maximum length of generated message
|
||||
cut_off = 100
|
||||
|
||||
# Write output to file instead of stdout, WARNING: will overwrite
|
||||
# output = "$HOME/.MOTD"
|
||||
|
||||
# Note: CLI arguments will override these RC file settings
|
||||
```
|
||||
|
||||
### Word Files
|
||||
|
||||
You can also create custom word files inside of `$XDG_CONFIG_HOME/motd/`:
|
||||
```
|
||||
$XDG_CONFIG_HOME/motd/
|
||||
|-nouns
|
||||
|-adjectives
|
||||
|-adverbs
|
||||
|-locations
|
||||
|-templates
|
||||
```
|
||||
You can dump the default word banks into their respectice word file and disable/add entries individually:
|
||||
|
||||
```
|
||||
mkdir -p $XDG_CONFIG_HOME/motd
|
||||
motd -p templates > $XDG_CONFIG_HOME/motd/templates
|
||||
motd -p nouns > $XDG_CONFIG_HOME/motd/nouns
|
||||
motd -p adjectives > $XDG_CONFIG_HOME/motd/adjectives
|
||||
motd -p adverbs > $XDG_CONFIG_HOME/motd/adverbs
|
||||
motd -p locations > $XDG_CONFIG_HOME/motd/locations
|
||||
```
|
||||
Words are separated by line, meaning you can have multiple words treated as a single unit.\
|
||||
Re-defining a word will not add an additional entry to the word pool.
|
||||
Lines beginning with a `!` are actively removed from their respective word pool.\
|
||||
Lines beginning with a `#` are ignored as comments.
|
||||
Lines with the format `$()` can be `bash` expressions which are evaluated at runtime.\
|
||||
Example: `nouns` file
|
||||
```
|
||||
# I'm really scared of these
|
||||
!ocean
|
||||
ocean
|
||||
# The previous ocean will still not be added to the word pool
|
||||
$(date +%H:%M)
|
||||
```
|
||||
|
||||
Now running `motd` will remove 'ocean' from the noun word pool, while also adding the possibilty of running `date +%H:%M` when replacing a template's noun, which will give the current time of day.
|
||||
|
||||
```
|
||||
motd -r # -r is to force the added nouns to be used, for this example
|
||||
When the moon rises, the mighty 19:58 shines in the misty valley.
|
||||
```
|
||||
|
||||
Word files don't have to live in your configuration directory, you can pass `motd` a custom `--configdir/-c`.
|
||||
|
||||
### Templates
|
||||
|
||||
Templates are separated by line. They can have as many or as few word types as you'd like.
|
||||
* `{noun}` or `{n}`
|
||||
* `{adjective}` or `{a}`
|
||||
* `{verb}` or `{v}`
|
||||
* `{adverb}` or `{d}`
|
||||
* `{location}` or `{l}`
|
||||
|
||||
Bash expressions (`$()` form) in the `templates` file are evaluate at runtime.
|
||||
|
||||
## Examples
|
||||
|
||||
```
|
||||
❯ motd -U -s . -s : -s , -D .
|
||||
THE.ETERNAL.PHOENIX.BLOOMS.SILENTLY.WHERE.SHADOWS.DANCE
|
||||
```
|
||||
`-U`: Capitalizes output\
|
||||
`-s`: Strips given character from generation\
|
||||
`-D`: Change delimiter from `space`\
|
||||
Note that the stripping of characters occurs first, then the delimiter is replaced with the argument.
|
||||
|
||||
```
|
||||
❯ motd -v opens --noun book -n chair -d weirdly -t "A {n} {v} the {noun} {d}" -r
|
||||
A chair opens the book weirdly
|
||||
```
|
||||
`--noun\-n\-v\-d`: Single word by type, to add to the word pool. Can be specified multiple times.\
|
||||
`-t`: The template to base the message off. Using `-t` will force the template used to be argument given.\
|
||||
`-r`: Only new words, passed as arguments, or from configuration files, will be used.
|
||||
|
||||
```
|
||||
❯ motd -t "The {n} and the {noun} and the {n}."
|
||||
The wizard and the knight and the ocean.
|
||||
```
|
||||
`-t`: Force template to be used.\
|
||||
`--noun/-n`: Adding multiple noun positions and nothing else.
|
||||
|
||||
```
|
||||
❯ motd -C 20 2>output.txt
|
||||
❯ echo $?
|
||||
1
|
||||
❯ cat output.txt
|
||||
Error: CutOffExceeded { attempts: 100, cut_off: 20 }
|
||||
```
|
||||
`-C`: Specify a max amount of characters the message can be.
|
||||
`motd` exits with `1` when it is unable to generate a valid message. Either the template pool is too diluted, or you create a valid template within the confines you want.
|
||||
150
src/cli.rs
Normal file
150
src/cli.rs
Normal file
@ -0,0 +1,150 @@
|
||||
use clap::Parser;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = env!("CARGO_PKG_NAME"))]
|
||||
#[command(version)]
|
||||
#[command(about = "Generate random messages with customizable word lists.")]
|
||||
pub struct Args {
|
||||
/// Specify configuration directory
|
||||
#[arg(short = 'c', long)]
|
||||
pub configdir: Option<String>,
|
||||
|
||||
/// Write output to file
|
||||
#[arg(short = 'o', long)]
|
||||
pub output: Option<String>,
|
||||
|
||||
/// Add an adjective
|
||||
#[arg(short = 'a', long)]
|
||||
pub adjective: Option<Vec<String>>,
|
||||
|
||||
/// Add a noun
|
||||
#[arg(short = 'n', long)]
|
||||
pub noun: Option<Vec<String>>,
|
||||
|
||||
/// Add a verb
|
||||
#[arg(short = 'v', long)]
|
||||
pub verb: Option<Vec<String>>,
|
||||
|
||||
/// Add an adverb
|
||||
#[arg(short = 'd', long)]
|
||||
pub adverb: Option<Vec<String>>,
|
||||
|
||||
/// Add a location
|
||||
#[arg(short = 'l', long)]
|
||||
pub location: Option<Vec<String>>,
|
||||
|
||||
/// Custom override template: "{(n)oun} {(a)djective} {(v)erb} {a(d)verb} {(l)ocation}"
|
||||
#[arg(short = 't', long)]
|
||||
pub template: Option<String>,
|
||||
|
||||
/// Replace default lists instead of extending
|
||||
#[arg(short = 'r', long)]
|
||||
pub replace: bool,
|
||||
|
||||
/// Lowercase output
|
||||
#[arg(short = 'L', long)]
|
||||
pub lowercase: bool,
|
||||
|
||||
/// Capitalize output
|
||||
#[arg(short = 'U', long)]
|
||||
pub uppercase: bool,
|
||||
|
||||
/// String to strip
|
||||
#[arg(short = 's', long)]
|
||||
pub strip: Option<Vec<String>>,
|
||||
|
||||
/// Delimiter
|
||||
#[arg(short = 'D', long)]
|
||||
pub delimiter: Option<String>,
|
||||
|
||||
/// Cut-off
|
||||
#[arg(short = 'C', long)]
|
||||
pub cut_off: Option<i32>,
|
||||
|
||||
/// Dump default lists
|
||||
#[arg(short = 'p', long, value_name = "WORD_TYPE")]
|
||||
pub print: Option<Option<String>>,
|
||||
|
||||
/// Outputs shell completion for the given shell
|
||||
#[arg(long, value_name = "SHELL")]
|
||||
pub completion: Option<clap_complete::Shell>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Debug)]
|
||||
pub struct RcArgs {
|
||||
pub output: Option<String>,
|
||||
pub adjective: Option<Vec<String>>,
|
||||
pub noun: Option<Vec<String>>,
|
||||
pub verb: Option<Vec<String>>,
|
||||
pub adverb: Option<Vec<String>>,
|
||||
pub location: Option<Vec<String>>,
|
||||
pub template: Option<String>,
|
||||
pub replace: Option<bool>,
|
||||
pub lowercase: Option<bool>,
|
||||
pub uppercase: Option<bool>,
|
||||
pub strip: Option<Vec<String>>,
|
||||
pub delimiter: Option<String>,
|
||||
pub cut_off: Option<i32>,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
pub fn merge_with_rc(mut self, rc_args: RcArgs) -> Self {
|
||||
// CLI args take precedence over RC file args
|
||||
// Only use RC file values if CLI values are None/false/default
|
||||
|
||||
if self.output.is_none() {
|
||||
self.output = rc_args.output;
|
||||
}
|
||||
|
||||
if self.adjective.is_none() {
|
||||
self.adjective = rc_args.adjective;
|
||||
}
|
||||
|
||||
if self.noun.is_none() {
|
||||
self.noun = rc_args.noun;
|
||||
}
|
||||
|
||||
if self.verb.is_none() {
|
||||
self.verb = rc_args.verb;
|
||||
}
|
||||
|
||||
if self.adverb.is_none() {
|
||||
self.adverb = rc_args.adverb;
|
||||
}
|
||||
|
||||
if self.location.is_none() {
|
||||
self.location = rc_args.location;
|
||||
}
|
||||
|
||||
if self.template.is_none() {
|
||||
self.template = rc_args.template;
|
||||
}
|
||||
|
||||
if !self.replace {
|
||||
self.replace = rc_args.replace.unwrap_or(false);
|
||||
}
|
||||
|
||||
if !self.lowercase {
|
||||
self.lowercase = rc_args.lowercase.unwrap_or(false);
|
||||
}
|
||||
|
||||
if !self.uppercase {
|
||||
self.uppercase = rc_args.uppercase.unwrap_or(false);
|
||||
}
|
||||
|
||||
if self.strip.is_none() {
|
||||
self.strip = rc_args.strip;
|
||||
}
|
||||
|
||||
if self.delimiter.is_none() {
|
||||
self.delimiter = rc_args.delimiter;
|
||||
}
|
||||
|
||||
if self.cut_off.is_none() {
|
||||
self.cut_off = rc_args.cut_off;
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
234
src/config.rs
Normal file
234
src/config.rs
Normal file
@ -0,0 +1,234 @@
|
||||
use crate::cli::RcArgs;
|
||||
use crate::words::WordList;
|
||||
use crate::words::{WordType, WORD_TYPES};
|
||||
use regex::Regex;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
pub struct Config {
|
||||
pub words: WordList,
|
||||
pub exclusions: WordList,
|
||||
pub templates: Vec<String>,
|
||||
pub template_exclusions: Vec<String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load(config_dir: Option<&str>) -> Self {
|
||||
let config_dir = if let Some(dir) = config_dir {
|
||||
PathBuf::from(dir)
|
||||
} else {
|
||||
get_config_dir()
|
||||
};
|
||||
|
||||
let mut words = WordList::new();
|
||||
let mut exclusions = WordList::new();
|
||||
let mut templates = Vec::new();
|
||||
let mut template_exclusions = Vec::new();
|
||||
for word_type in WORD_TYPES {
|
||||
let (word_list, exclusion_list) = load_word_list_with_exclusions(&config_dir.join(word_type.name()));
|
||||
if !word_list.is_empty() {
|
||||
match word_type {
|
||||
WordType::Adjective => words.adjectives = Some(word_list),
|
||||
WordType::Noun => words.nouns = Some(word_list),
|
||||
WordType::Verb => words.verbs = Some(word_list),
|
||||
WordType::Adverb => words.adverbs = Some(word_list),
|
||||
WordType::Location => words.locations = Some(word_list),
|
||||
WordType::Template => templates = word_list,
|
||||
}
|
||||
}
|
||||
if !exclusion_list.is_empty() {
|
||||
match word_type {
|
||||
WordType::Adjective => exclusions.adjectives = Some(exclusion_list),
|
||||
WordType::Noun => exclusions.nouns = Some(exclusion_list),
|
||||
WordType::Verb => exclusions.verbs = Some(exclusion_list),
|
||||
WordType::Adverb => exclusions.adverbs = Some(exclusion_list),
|
||||
WordType::Location => exclusions.locations = Some(exclusion_list),
|
||||
WordType::Template => template_exclusions = exclusion_list,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Config {
|
||||
words,
|
||||
exclusions,
|
||||
templates,
|
||||
template_exclusions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_config_dir() -> PathBuf {
|
||||
if let Ok(xdg_config) = env::var("XDG_CONFIG_HOME") {
|
||||
PathBuf::from(xdg_config).join(env!("CARGO_PKG_NAME"))
|
||||
} else if let Ok(home) = env::var("HOME") {
|
||||
PathBuf::from(home)
|
||||
.join(".config")
|
||||
.join(env!("CARGO_PKG_NAME"))
|
||||
} else {
|
||||
PathBuf::from(".config").join(env!("CARGO_PKG_NAME"))
|
||||
}
|
||||
}
|
||||
|
||||
fn load_word_list_with_exclusions(path: &PathBuf) -> (Vec<String>, Vec<String>) {
|
||||
match fs::read_to_string(path) {
|
||||
Ok(content) => {
|
||||
let lines: Vec<String> = content
|
||||
.lines()
|
||||
.map(|line| line.trim().to_string())
|
||||
.filter(|line| !line.is_empty() && !line.starts_with('#'))
|
||||
.map(|line| expand_line(&line))
|
||||
.collect();
|
||||
|
||||
// Separate exclusions (lines starting with '!') from regular words
|
||||
let mut exclusions = Vec::new();
|
||||
let mut words = Vec::new();
|
||||
|
||||
for line in lines {
|
||||
if line.starts_with('!') {
|
||||
// Remove the '!' prefix and add to exclusions
|
||||
exclusions.push(line[1..].to_string());
|
||||
} else {
|
||||
words.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out any words that are in the exclusion list
|
||||
let filtered_words = words.into_iter()
|
||||
.filter(|word| !exclusions.contains(word))
|
||||
.collect();
|
||||
|
||||
(filtered_words, exclusions)
|
||||
},
|
||||
Err(_) => (Vec::new(), Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn expand_line(line: &str) -> String {
|
||||
let mut result = line.to_string();
|
||||
|
||||
result = expand_env_vars(&result);
|
||||
result = expand_bash_expressions(&result);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn expand_env_vars(text: &str) -> String {
|
||||
let re = Regex::new(r"\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*)").unwrap();
|
||||
|
||||
re.replace_all(text, |caps: ®ex::Captures| {
|
||||
let var_name = match caps.get(1).or_else(|| caps.get(2)) {
|
||||
Some(v) => v.as_str(),
|
||||
None => return "".to_string(),
|
||||
};
|
||||
env::var(var_name).unwrap_or_else(|_| format!("${{{}}}", var_name))
|
||||
})
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn expand_bash_expressions(text: &str) -> String {
|
||||
let re = Regex::new(r"\$\(([^)]+)\)|`([^`]+)`").unwrap();
|
||||
|
||||
re.replace_all(text, |caps: ®ex::Captures| {
|
||||
let command = match caps.get(1).or_else(|| caps.get(2)) {
|
||||
Some(c) => c.as_str(),
|
||||
None => return "".to_string(),
|
||||
};
|
||||
execute_bash_command(command)
|
||||
})
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn execute_bash_command(command: &str) -> String {
|
||||
if is_recursive_command(command) {
|
||||
return "nice try".to_string();
|
||||
}
|
||||
|
||||
match Command::new("sh").arg("-c").arg(command).output() {
|
||||
Ok(output) => {
|
||||
if output.status.success() {
|
||||
String::from_utf8_lossy(&output.stdout).trim().to_string()
|
||||
} else {
|
||||
format!("$({})", command)
|
||||
}
|
||||
}
|
||||
Err(_) => format!("$({})", command),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_recursive_command(command: &str) -> bool {
|
||||
let current_exe = env::current_exe().unwrap_or_default();
|
||||
let program_name = current_exe
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("");
|
||||
|
||||
if program_name.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let tokens: Vec<&str> = command.split_whitespace().collect();
|
||||
|
||||
for token in tokens {
|
||||
if token == program_name {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(command_name) = token.split('/').last() {
|
||||
if command_name == program_name {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn load_rc_file() -> RcArgs {
|
||||
let rc_locations = get_rc_locations();
|
||||
|
||||
for path in rc_locations {
|
||||
if path.exists() {
|
||||
match fs::read_to_string(&path) {
|
||||
Ok(content) => {
|
||||
match toml::from_str::<RcArgs>(&content) {
|
||||
Ok(rc_args) => return rc_args,
|
||||
Err(e) => {
|
||||
eprintln!("Warning: Failed to parse RC file '{}': {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Warning: Failed to read RC file '{}': {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RcArgs::default()
|
||||
}
|
||||
|
||||
fn get_rc_locations() -> Vec<PathBuf> {
|
||||
let mut locations = Vec::new();
|
||||
|
||||
// ~/.motdrc
|
||||
if let Ok(home) = env::var("HOME") {
|
||||
locations.push(PathBuf::from(home).join(".motdrc"));
|
||||
}
|
||||
|
||||
// $XDG_CONFIG_HOME/.motdrc
|
||||
if let Ok(xdg_config) = env::var("XDG_CONFIG_HOME") {
|
||||
locations.push(PathBuf::from(xdg_config).join(".motdrc"));
|
||||
}
|
||||
|
||||
// $XDG_CONFIG_HOME/motd/motdrc
|
||||
if let Ok(xdg_config) = env::var("XDG_CONFIG_HOME") {
|
||||
locations.push(PathBuf::from(xdg_config).join("motd").join("motdrc"));
|
||||
} else if let Ok(home) = env::var("HOME") {
|
||||
// Fallback to ~/.config/motd/motdrc if XDG_CONFIG_HOME is not set
|
||||
locations.push(PathBuf::from(home).join(".config").join("motd").join("motdrc"));
|
||||
}
|
||||
|
||||
locations
|
||||
}
|
||||
255
src/generator.rs
Normal file
255
src/generator.rs
Normal file
@ -0,0 +1,255 @@
|
||||
use crate::cli::Args;
|
||||
use crate::config::Config;
|
||||
use crate::message_builder::MessageBuilder;
|
||||
use crate::templates::TEMPLATES;
|
||||
use crate::words::{
|
||||
WORD_TYPES, WordList, WordType, adjectives::ADJECTIVES, adverbs::ADVERBS, locations::LOCATIONS,
|
||||
nouns::NOUNS, verbs::VERBS,
|
||||
};
|
||||
use rand::Rng;
|
||||
use rand::seq::SliceRandom;
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GeneratorError {
|
||||
EmptyTemplateList,
|
||||
CutOffExceeded { attempts: usize, cut_off: i32 },
|
||||
}
|
||||
|
||||
impl fmt::Display for GeneratorError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
GeneratorError::EmptyTemplateList => write!(
|
||||
f,
|
||||
"No templates available. Provide a template or use the default ones."
|
||||
),
|
||||
GeneratorError::CutOffExceeded { attempts, cut_off } => write!(
|
||||
f,
|
||||
"Could not generate a message within {} characters after {} attempts.",
|
||||
cut_off, attempts
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for GeneratorError {}
|
||||
|
||||
const MAX_CUTOFF_ATTEMPTS: usize = 100;
|
||||
|
||||
pub fn generate_message(arg_words: WordList, args: Args) -> Result<String, GeneratorError> {
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
// Load config files
|
||||
let config = Config::load(args.configdir.as_deref());
|
||||
|
||||
// Get available templates
|
||||
let templates = if args.template.is_some() {
|
||||
vec![args.template.as_ref().unwrap().clone()]
|
||||
} else {
|
||||
let template_list = build_template_list(&config.templates, &config.template_exclusions);
|
||||
if template_list.is_empty() {
|
||||
return Err(GeneratorError::EmptyTemplateList);
|
||||
}
|
||||
template_list
|
||||
};
|
||||
|
||||
// Build word lists once
|
||||
let word_lists: HashMap<WordType, Vec<String>> = WORD_TYPES
|
||||
.iter()
|
||||
.filter(|&word_type| *word_type != WordType::Template)
|
||||
.map(|&word_type| {
|
||||
let (defaults, config_words, custom_words, exclusions) = match word_type {
|
||||
WordType::Adjective => (
|
||||
ADJECTIVES,
|
||||
config.words.adjectives.as_deref(),
|
||||
arg_words.adjectives.as_ref(),
|
||||
config.exclusions.adjectives.as_deref(),
|
||||
),
|
||||
WordType::Noun => (
|
||||
NOUNS,
|
||||
config.words.nouns.as_deref(),
|
||||
arg_words.nouns.as_ref(),
|
||||
config.exclusions.nouns.as_deref(),
|
||||
),
|
||||
WordType::Verb => (
|
||||
VERBS,
|
||||
config.words.verbs.as_deref(),
|
||||
arg_words.verbs.as_ref(),
|
||||
config.exclusions.verbs.as_deref(),
|
||||
),
|
||||
WordType::Adverb => (
|
||||
ADVERBS,
|
||||
config.words.adverbs.as_deref(),
|
||||
arg_words.adverbs.as_ref(),
|
||||
config.exclusions.adverbs.as_deref(),
|
||||
),
|
||||
WordType::Location => (
|
||||
LOCATIONS,
|
||||
config.words.locations.as_deref(),
|
||||
arg_words.locations.as_ref(),
|
||||
config.exclusions.locations.as_deref(),
|
||||
),
|
||||
_ => (NOUNS, None, None, None),
|
||||
};
|
||||
(
|
||||
word_type,
|
||||
build_word_list(defaults, config_words, custom_words, exclusions, args.replace),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let cut_off = args.cut_off;
|
||||
let mut attempts = 0;
|
||||
|
||||
loop {
|
||||
attempts += 1;
|
||||
|
||||
// Select a template
|
||||
let template = templates[rng.gen_range(0..templates.len())].clone();
|
||||
|
||||
// Analyze template to count placeholders
|
||||
let placeholder_counts = analyze_template(&template);
|
||||
|
||||
let all_words: HashMap<WordType, Vec<String>> = WORD_TYPES
|
||||
.iter()
|
||||
.filter_map(|&word_type| {
|
||||
let count = *placeholder_counts.get(&word_type).unwrap_or(&0);
|
||||
if count > 0 {
|
||||
word_lists
|
||||
.get(&word_type)
|
||||
.map(|list| (word_type, generate_words(list, count, &mut rng)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Replace placeholders with generated words
|
||||
let builder = replace_placeholders(&template, &all_words);
|
||||
|
||||
let mut message = MessageBuilder::new(builder)
|
||||
.remove_duplicates()
|
||||
.fix_articles()
|
||||
.fix_s()
|
||||
.capitalize_sentences(args.uppercase, args.lowercase);
|
||||
|
||||
if let Some(ref strip) = args.strip {
|
||||
message = message.strip_words(strip);
|
||||
}
|
||||
|
||||
if let Some(ref delimiter) = args.delimiter {
|
||||
message = message.replace_delimiter(delimiter.as_str());
|
||||
}
|
||||
|
||||
let final_message = message.build();
|
||||
|
||||
// Check cut-off if specified
|
||||
if let Some(cut_off_len) = cut_off {
|
||||
if final_message.len() <= cut_off_len as usize {
|
||||
return Ok(final_message);
|
||||
}
|
||||
|
||||
// If we've exceeded max attempts, return an error
|
||||
if attempts >= MAX_CUTOFF_ATTEMPTS {
|
||||
return Err(GeneratorError::CutOffExceeded {
|
||||
attempts,
|
||||
cut_off: cut_off_len
|
||||
});
|
||||
}
|
||||
|
||||
// Continue the loop to try again
|
||||
continue;
|
||||
} else {
|
||||
// No cut-off specified, return the message
|
||||
return Ok(final_message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_word_list(
|
||||
defaults: &[&str],
|
||||
config_words: Option<&[String]>,
|
||||
custom: Option<&Vec<String>>,
|
||||
exclusions: Option<&[String]>,
|
||||
replace: bool,
|
||||
) -> Vec<String> {
|
||||
let mut words = if replace {
|
||||
if let Some(custom_words) = custom.filter(|v| !v.is_empty()) {
|
||||
custom_words.clone()
|
||||
} else if let Some(config_words) = config_words.filter(|v| !v.is_empty()) {
|
||||
config_words.to_vec()
|
||||
} else {
|
||||
defaults.iter().map(|s| s.to_string()).collect()
|
||||
}
|
||||
} else {
|
||||
defaults
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.chain(config_words.into_iter().flatten().cloned())
|
||||
.chain(custom.into_iter().flatten().cloned())
|
||||
.collect()
|
||||
};
|
||||
|
||||
// Apply exclusions if they exist
|
||||
if let Some(exclusion_list) = exclusions {
|
||||
words.retain(|word| !exclusion_list.contains(word));
|
||||
}
|
||||
|
||||
words
|
||||
}
|
||||
|
||||
fn build_template_list(config_templates: &[String], exclusions: &[String]) -> Vec<String> {
|
||||
let mut templates: Vec<String> = TEMPLATES.iter().map(|s| s.to_string()).collect();
|
||||
templates.extend(config_templates.iter().cloned());
|
||||
|
||||
// Apply exclusions if they exist
|
||||
if !exclusions.is_empty() {
|
||||
templates.retain(|template| !exclusions.contains(template));
|
||||
}
|
||||
|
||||
templates
|
||||
}
|
||||
|
||||
fn analyze_template(template: &str) -> HashMap<WordType, usize> {
|
||||
WORD_TYPES
|
||||
.iter()
|
||||
.map(|&word_type| {
|
||||
let count = template.matches(word_type.long_placeholder()).count()
|
||||
+ template.matches(word_type.short_placeholder()).count();
|
||||
(word_type, count)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn generate_words<R: Rng>(word_list: &[String], count: usize, rng: &mut R) -> Vec<String> {
|
||||
if count <= word_list.len() {
|
||||
word_list.choose_multiple(rng, count).cloned().collect()
|
||||
} else {
|
||||
(0..count)
|
||||
.filter_map(|_| word_list.choose(rng).cloned())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_placeholders(template: &str, all_words: &HashMap<WordType, Vec<String>>) -> String {
|
||||
let mut builder = template.to_string();
|
||||
|
||||
for &word_type in &WORD_TYPES {
|
||||
builder = builder.replace(word_type.short_placeholder(), word_type.long_placeholder());
|
||||
|
||||
if let Some(words) = all_words.get(&word_type) {
|
||||
let mut word_iter = words.iter();
|
||||
while let Some(pos) = builder.find(word_type.long_placeholder()) {
|
||||
if let Some(word) = word_iter.next() {
|
||||
builder.replace_range(pos..pos + word_type.long_placeholder().len(), word);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builder
|
||||
}
|
||||
118
src/main.rs
Normal file
118
src/main.rs
Normal file
@ -0,0 +1,118 @@
|
||||
use clap::{CommandFactory, Parser};
|
||||
use cli::Args;
|
||||
use config::load_rc_file;
|
||||
use words::WordList;
|
||||
|
||||
mod cli;
|
||||
mod config;
|
||||
mod generator;
|
||||
mod message_builder;
|
||||
mod templates;
|
||||
mod text_utils;
|
||||
mod words;
|
||||
|
||||
use generator::generate_message;
|
||||
use templates::TEMPLATES;
|
||||
use words::{adjectives, adverbs, locations, nouns, verbs};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Load RC file configuration first
|
||||
let rc_args = load_rc_file();
|
||||
|
||||
// Parse CLI args and merge with RC file defaults
|
||||
let mut args = Args::parse().merge_with_rc(rc_args);
|
||||
|
||||
if let Some(shell) = args.completion {
|
||||
let mut cmd = <Args as CommandFactory>::command();
|
||||
let name = cmd.get_name().to_string();
|
||||
clap_complete::generate(shell, &mut cmd, name, &mut std::io::stdout());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(print_arg) = args.print {
|
||||
match print_arg {
|
||||
Some(word_type) => {
|
||||
// Print specific word type without headers
|
||||
match word_type.to_lowercase().as_str() {
|
||||
"adjectives" | "adjective" => {
|
||||
for adj in adjectives::ADJECTIVES {
|
||||
println!("{}", adj);
|
||||
}
|
||||
}
|
||||
"nouns" | "noun" => {
|
||||
for noun in nouns::NOUNS {
|
||||
println!("{}", noun);
|
||||
}
|
||||
}
|
||||
"verbs" | "verb" => {
|
||||
for verb in verbs::VERBS {
|
||||
println!("{}", verb);
|
||||
}
|
||||
}
|
||||
"adverbs" | "adverb" => {
|
||||
for adverb in adverbs::ADVERBS {
|
||||
println!("{}", adverb);
|
||||
}
|
||||
}
|
||||
"locations" | "location" => {
|
||||
for location in locations::LOCATIONS {
|
||||
println!("{}", location);
|
||||
}
|
||||
}
|
||||
"templates" | "template" => {
|
||||
for template in TEMPLATES {
|
||||
println!("{}", template);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
eprintln!(
|
||||
"Unknown word type: {}. Valid types are: adjectives, nouns, verbs, adverbs, locations, templates",
|
||||
word_type
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Print all lists with headers (original behavior)
|
||||
println!(
|
||||
"-------Adjectives------\n{}",
|
||||
adjectives::ADJECTIVES.join("\n")
|
||||
);
|
||||
println!("---------Nouns---------\n{}", nouns::NOUNS.join("\n"));
|
||||
println!("---------Verbs---------\n{}", verbs::VERBS.join("\n"));
|
||||
println!("--------Adverbs--------\n{}", adverbs::ADVERBS.join("\n"));
|
||||
println!(
|
||||
"-------Locations-------\n{}",
|
||||
locations::LOCATIONS.join("\n")
|
||||
);
|
||||
println!("-------Templates-------\n{}", TEMPLATES.join("\n"));
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let word_list = WordList {
|
||||
adjectives: args.adjective.take(),
|
||||
nouns: args.noun.take(),
|
||||
verbs: args.verb.take(),
|
||||
adverbs: args.adverb.take(),
|
||||
locations: args.location.take(),
|
||||
};
|
||||
let output = args.output.take();
|
||||
|
||||
let message = generate_message(word_list, args)?;
|
||||
match output {
|
||||
Some(output_path) => {
|
||||
std::fs::write(&output_path, format!("{}\n", message)).unwrap_or_else(|e| {
|
||||
eprintln!("Failed to write to file '{}': {}", output_path, e);
|
||||
std::process::exit(1);
|
||||
});
|
||||
}
|
||||
None => {
|
||||
println!("{}", message);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
47
src/message_builder.rs
Normal file
47
src/message_builder.rs
Normal file
@ -0,0 +1,47 @@
|
||||
use crate::text_utils::*;
|
||||
|
||||
pub struct MessageBuilder {
|
||||
content: String,
|
||||
}
|
||||
|
||||
impl MessageBuilder {
|
||||
pub fn new(content: String) -> Self {
|
||||
Self { content }
|
||||
}
|
||||
|
||||
pub fn fix_articles(mut self) -> Self {
|
||||
self.content = fix_articles(&self.content);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn capitalize_sentences(mut self, uppercase: bool, lowercase: bool) -> Self {
|
||||
self.content = capitalize_sentences(&self.content, uppercase, lowercase);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn fix_s(mut self) -> Self {
|
||||
self.content = fix_s(&self.content);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn remove_duplicates(mut self) -> Self {
|
||||
self.content = remove_duplicates(&self.content);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn replace_delimiter(mut self, delimiter: &str) -> Self {
|
||||
self.content = self.content.replace(" ", delimiter);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn strip_words(mut self, words: &Vec<String>) -> Self {
|
||||
for word in words {
|
||||
self.content = self.content.replace(word, "");
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> String {
|
||||
self.content
|
||||
}
|
||||
}
|
||||
13
src/templates.rs
Normal file
13
src/templates.rs
Normal file
@ -0,0 +1,13 @@
|
||||
pub const TEMPLATES: &[&str] = &[
|
||||
"The {adjective} {noun} {verb} {adverb} {location}.",
|
||||
"Today, the {adjective} {noun} {verb} {location}.",
|
||||
"Behold! {adjective} {noun} {verb} {adverb}.",
|
||||
"In ancient times, the {adjective} {noun} {verb} {adverb} {location}.",
|
||||
"Legend speaks of {adjective} {noun} that {verb} {location}.",
|
||||
"The {adjective} {noun} {verb} {adverb}, bringing peace to all.",
|
||||
"When the moon rises, the {adjective} {noun} {verb} {location}.",
|
||||
"Remember: even the {adjective} {noun} {verb} {adverb}.",
|
||||
"The wise say that {adjective} {noun} always {verb} {location}.",
|
||||
"Every {adjective} {noun} {verb} {adverb} when the time is right.",
|
||||
"A {adjective} {noun} {verb} {adverb} {location}.",
|
||||
];
|
||||
215
src/text_utils.rs
Normal file
215
src/text_utils.rs
Normal file
@ -0,0 +1,215 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub fn starts_with_vowel(word: &str) -> bool {
|
||||
let first_char = word
|
||||
.chars()
|
||||
.next()
|
||||
.unwrap_or(' ')
|
||||
.to_lowercase()
|
||||
.next()
|
||||
.unwrap_or(' ');
|
||||
matches!(first_char, 'a' | 'e' | 'i' | 'o' | 'u')
|
||||
}
|
||||
|
||||
pub fn fix_articles(text: &str) -> String {
|
||||
let mut result = String::new();
|
||||
let words: Vec<&str> = text.split_whitespace().collect();
|
||||
|
||||
for (i, word) in words.iter().enumerate() {
|
||||
if i > 0 {
|
||||
result.push(' ');
|
||||
}
|
||||
|
||||
if (word.eq_ignore_ascii_case("a") || word.eq_ignore_ascii_case("an"))
|
||||
&& i + 1 < words.len()
|
||||
{
|
||||
let next_word = words[i + 1];
|
||||
if starts_with_vowel(next_word) {
|
||||
result.push_str("an");
|
||||
} else {
|
||||
result.push('a');
|
||||
}
|
||||
} else {
|
||||
result.push_str(word);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn capitalize_sentences(text: &str, uppercase: bool, lowercase: bool) -> String {
|
||||
let mut result = String::new();
|
||||
let mut capitalize_next = true;
|
||||
|
||||
match (uppercase, lowercase) {
|
||||
(true, false) => result = text.to_uppercase(),
|
||||
(false, true) => result = text.to_lowercase(),
|
||||
_ => {
|
||||
for ch in text.chars() {
|
||||
if capitalize_next && ch.is_alphabetic() {
|
||||
result.push(ch.to_uppercase().next().unwrap_or(ch));
|
||||
capitalize_next = false;
|
||||
} else {
|
||||
result.push(ch);
|
||||
if matches!(ch, '.' | '!' | '?') {
|
||||
capitalize_next = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
static SINGULAR_SUBJECTS: &[&str] = &["he", "she", "it", "this", "that", "one"];
|
||||
static PLURAL_SUBJECTS: &[&str] = &[
|
||||
"they", "we", "you", "these", "those", "many", "several", "few", "both", "all", "some",
|
||||
];
|
||||
static PLURAL_NUMBERS: &[&str] = &[
|
||||
"two", "three", "four", "five", "six", "seven", "eight", "nine", "ten",
|
||||
];
|
||||
static PLURAL_ADVERBS: &[&str] = &[
|
||||
"always",
|
||||
"never",
|
||||
"often",
|
||||
"usually",
|
||||
"sometimes",
|
||||
"rarely",
|
||||
"frequently",
|
||||
"occasionally",
|
||||
];
|
||||
|
||||
// Protected nouns that should never lose their 's'
|
||||
static PROTECTED_NOUNS: &[&str] = &[
|
||||
"crisis",
|
||||
"basis",
|
||||
"analysis",
|
||||
"thesis",
|
||||
"synthesis",
|
||||
"emphasis",
|
||||
"oasis",
|
||||
"synopsis",
|
||||
"diagnosis",
|
||||
"prognosis",
|
||||
"paralysis",
|
||||
"class",
|
||||
"glass",
|
||||
"grass",
|
||||
"mass",
|
||||
"pass",
|
||||
"business",
|
||||
"process",
|
||||
"address",
|
||||
"success",
|
||||
"access",
|
||||
"progress",
|
||||
"fitness",
|
||||
"illness",
|
||||
"darkness",
|
||||
"kindness",
|
||||
"goodness",
|
||||
"awareness",
|
||||
"fairness",
|
||||
];
|
||||
|
||||
pub fn fix_s(text: &str) -> String {
|
||||
let protected_set: HashSet<&str> = PROTECTED_NOUNS.iter().copied().collect();
|
||||
let singular_set: HashSet<&str> = SINGULAR_SUBJECTS.iter().copied().collect();
|
||||
let plural_set: HashSet<&str> = PLURAL_SUBJECTS
|
||||
.iter()
|
||||
.chain(PLURAL_NUMBERS.iter())
|
||||
.chain(PLURAL_ADVERBS.iter())
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
let words: Vec<&str> = text.split_whitespace().collect();
|
||||
let mut result = String::new();
|
||||
|
||||
for (i, word) in words.iter().enumerate() {
|
||||
if i > 0 {
|
||||
result.push(' ');
|
||||
}
|
||||
|
||||
if i > 0 && (word.ends_with('s') || word.ends_with("es")) {
|
||||
let word_lower = word.to_lowercase();
|
||||
|
||||
// Skip protected nouns
|
||||
if protected_set.contains(word_lower.as_str()) {
|
||||
result.push_str(word);
|
||||
continue;
|
||||
}
|
||||
|
||||
let prev_word = words[i - 1].to_lowercase();
|
||||
|
||||
let should_remove_s = if let Ok(num) = prev_word.parse::<i32>() {
|
||||
num != 1
|
||||
} else if plural_set.contains(prev_word.as_str()) {
|
||||
true
|
||||
} else if singular_set.contains(prev_word.as_str()) {
|
||||
false
|
||||
} else {
|
||||
// Default: keep 's' for unknown cases
|
||||
false
|
||||
};
|
||||
|
||||
if should_remove_s {
|
||||
result.push_str(&conjugate_to_base_form(word));
|
||||
} else {
|
||||
result.push_str(word);
|
||||
}
|
||||
} else {
|
||||
result.push_str(word);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn conjugate_to_base_form(word: &str) -> String {
|
||||
match word {
|
||||
"goes" => "go".to_string(),
|
||||
"does" => "do".to_string(),
|
||||
"is" => "are".to_string(),
|
||||
"has" => "have".to_string(),
|
||||
_ => {
|
||||
if word.ends_with("ies") && word.len() > 3 {
|
||||
// flies -> fly, tries -> try
|
||||
format!("{}y", &word[..word.len() - 3])
|
||||
} else if word.ends_with("ches")
|
||||
|| word.ends_with("shes")
|
||||
|| word.ends_with("xes")
|
||||
|| word.ends_with("zes")
|
||||
|| word.ends_with("ses")
|
||||
{
|
||||
// watches -> watch, fixes -> fix, focuses -> focus
|
||||
word[..word.len() - 2].to_string()
|
||||
} else if word.ends_with("oes") && word.len() > 3 {
|
||||
// echoes -> echo, heroes -> hero, potatoes -> potato
|
||||
word[..word.len() - 2].to_string()
|
||||
} else if word.ends_with('s') && word.len() > 1 {
|
||||
// runs -> run, sparkles -> sparkle
|
||||
word[..word.len() - 1].to_string()
|
||||
} else {
|
||||
word.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_duplicates(text: &str) -> String {
|
||||
let mut result = String::new();
|
||||
let words: Vec<&str> = text.split_whitespace().collect();
|
||||
|
||||
for (i, word) in words.iter().enumerate() {
|
||||
if i > 0 {
|
||||
result.push(' ');
|
||||
}
|
||||
|
||||
if i == 0 || *word != words[i - 1] {
|
||||
result.push_str(word);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
80
src/words.rs
Normal file
80
src/words.rs
Normal file
@ -0,0 +1,80 @@
|
||||
pub mod adjectives;
|
||||
pub mod adverbs;
|
||||
pub mod locations;
|
||||
pub mod nouns;
|
||||
pub mod verbs;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum WordType {
|
||||
Adjective,
|
||||
Noun,
|
||||
Verb,
|
||||
Adverb,
|
||||
Location,
|
||||
Template,
|
||||
}
|
||||
|
||||
impl WordType {
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
WordType::Adjective => "adjectives",
|
||||
WordType::Noun => "nouns",
|
||||
WordType::Verb => "verbs",
|
||||
WordType::Adverb => "adverbs",
|
||||
WordType::Location => "locations",
|
||||
WordType::Template => "templates",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn long_placeholder(&self) -> &'static str {
|
||||
match self {
|
||||
WordType::Adjective => "{adjective}",
|
||||
WordType::Noun => "{noun}",
|
||||
WordType::Verb => "{verb}",
|
||||
WordType::Adverb => "{adverb}",
|
||||
WordType::Location => "{location}",
|
||||
WordType::Template => "",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn short_placeholder(&self) -> &'static str {
|
||||
match self {
|
||||
WordType::Adjective => "{a}",
|
||||
WordType::Noun => "{n}",
|
||||
WordType::Verb => "{v}",
|
||||
WordType::Adverb => "{d}",
|
||||
WordType::Location => "{l}",
|
||||
WordType::Template => "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const WORD_TYPES: [WordType; 6] = [
|
||||
WordType::Adjective,
|
||||
WordType::Noun,
|
||||
WordType::Verb,
|
||||
WordType::Adverb,
|
||||
WordType::Location,
|
||||
WordType::Template,
|
||||
];
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct WordList {
|
||||
pub adjectives: Option<Vec<String>>,
|
||||
pub nouns: Option<Vec<String>>,
|
||||
pub verbs: Option<Vec<String>>,
|
||||
pub adverbs: Option<Vec<String>>,
|
||||
pub locations: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl WordList {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
adjectives: None,
|
||||
nouns: None,
|
||||
verbs: None,
|
||||
adverbs: None,
|
||||
locations: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/words/adjectives.rs
Normal file
23
src/words/adjectives.rs
Normal file
@ -0,0 +1,23 @@
|
||||
pub const ADJECTIVES: &[&str] = &[
|
||||
"brilliant",
|
||||
"mysterious",
|
||||
"ancient",
|
||||
"shimmering",
|
||||
"peaceful",
|
||||
"mighty",
|
||||
"clever",
|
||||
"graceful",
|
||||
"bold",
|
||||
"serene",
|
||||
"vibrant",
|
||||
"wise",
|
||||
"swift",
|
||||
"gentle",
|
||||
"fierce",
|
||||
"luminous",
|
||||
"silent",
|
||||
"eternal",
|
||||
"magnificent",
|
||||
"curious",
|
||||
// More adjectives
|
||||
];
|
||||
20
src/words/adverbs.rs
Normal file
20
src/words/adverbs.rs
Normal file
@ -0,0 +1,20 @@
|
||||
pub const ADVERBS: &[&str] = &[
|
||||
"silently",
|
||||
"gracefully",
|
||||
"powerfully",
|
||||
"mysteriously",
|
||||
"peacefully",
|
||||
"boldly",
|
||||
"gently",
|
||||
"swiftly",
|
||||
"wisely",
|
||||
"eternally",
|
||||
"brilliantly",
|
||||
"quietly",
|
||||
"majestically",
|
||||
"softly",
|
||||
"fiercely",
|
||||
"calmly",
|
||||
"steadily",
|
||||
"proudly",
|
||||
];
|
||||
14
src/words/locations.rs
Normal file
14
src/words/locations.rs
Normal file
@ -0,0 +1,14 @@
|
||||
pub const LOCATIONS: &[&str] = &[
|
||||
"in the misty valley",
|
||||
"beneath the starlit sky",
|
||||
"beyond the ancient ruins",
|
||||
"within the enchanted grove",
|
||||
"atop the highest peak",
|
||||
"beside the crystal lake",
|
||||
"through the whispering woods",
|
||||
"across the golden plains",
|
||||
"near the forgotten temple",
|
||||
"in the realm of dreams",
|
||||
"at the edge of time",
|
||||
"where shadows dance",
|
||||
];
|
||||
5
src/words/nouns.rs
Normal file
5
src/words/nouns.rs
Normal file
@ -0,0 +1,5 @@
|
||||
pub const NOUNS: &[&str] = &[
|
||||
"dragon", "castle", "forest", "ocean", "mountain", "wizard", "knight", "phoenix", "crystal",
|
||||
"portal", "garden", "library", "tower", "bridge", "temple", "compass", "lantern", "scroll",
|
||||
"mirror", "key", "crown", "sword", "shield", "staff", // More nouns
|
||||
];
|
||||
23
src/words/verbs.rs
Normal file
23
src/words/verbs.rs
Normal file
@ -0,0 +1,23 @@
|
||||
pub const VERBS: &[&str] = &[
|
||||
"awakens",
|
||||
"glows",
|
||||
"whispers",
|
||||
"dances",
|
||||
"soars",
|
||||
"guards",
|
||||
"seeks",
|
||||
"remembers",
|
||||
"shines",
|
||||
"flows",
|
||||
"stands",
|
||||
"calls",
|
||||
"watches",
|
||||
"guides",
|
||||
"protects",
|
||||
"reveals",
|
||||
"echoes",
|
||||
"blooms",
|
||||
"sparkles",
|
||||
"wanders",
|
||||
// More verbs
|
||||
];
|
||||
Loading…
x
Reference in New Issue
Block a user