Session handles are one word from a single 218-entry list — instruments, forms, notation and theory, tempo and dynamics — replacing the adjective-noun pair. A batch is shuffled, so it repeats only once the wordlist runs out. Project boxes show the parent directory instead of the full path: the last segment is already the name line above it. The last command is gone from the box, and picking a project no longer pre-selects it — the directory and what runs in it are independent choices. Both changes land in the TUI picker too, which mirrors the web form's rules. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
301 lines
5.8 KiB
Rust
301 lines
5.8 KiB
Rust
//! The one home of the session-handle wordlist (#53).
|
|
//!
|
|
//! Previously duplicated in `web/src/names.ts`; every client now gets handles
|
|
//! from here, served by `GET /api/names`.
|
|
|
|
/// Every word a handle can be: instruments, forms, and the vocabulary of
|
|
/// tempo, dynamics, notation and theory. One word per handle — the list is
|
|
/// wide enough that a single word still reads as distinct.
|
|
pub const WORDS: &[&str] = &[
|
|
// Instruments
|
|
"cello",
|
|
"violin",
|
|
"viola",
|
|
"oboe",
|
|
"flute",
|
|
"piccolo",
|
|
"clarinet",
|
|
"bassoon",
|
|
"trumpet",
|
|
"trombone",
|
|
"tuba",
|
|
"horn",
|
|
"cornet",
|
|
"bugle",
|
|
"flugelhorn",
|
|
"euphonium",
|
|
"saxophone",
|
|
"recorder",
|
|
"timpani",
|
|
"harp",
|
|
"piano",
|
|
"organ",
|
|
"lyre",
|
|
"lute",
|
|
"mandolin",
|
|
"banjo",
|
|
"ukulele",
|
|
"guitar",
|
|
"sitar",
|
|
"koto",
|
|
"shamisen",
|
|
"balalaika",
|
|
"zither",
|
|
"dulcimer",
|
|
"kantele",
|
|
"erhu",
|
|
"guzheng",
|
|
"pipa",
|
|
"duduk",
|
|
"ocarina",
|
|
"kalimba",
|
|
"harmonica",
|
|
"accordion",
|
|
"bandoneon",
|
|
"bagpipe",
|
|
"harpsichord",
|
|
"clavichord",
|
|
"celesta",
|
|
"theremin",
|
|
"marimba",
|
|
"xylophone",
|
|
"vibraphone",
|
|
"glockenspiel",
|
|
"snare",
|
|
"cymbal",
|
|
"tambourine",
|
|
"castanet",
|
|
"djembe",
|
|
"tabla",
|
|
"bodhran",
|
|
"gong",
|
|
"chimes",
|
|
"contrabass",
|
|
"shawm",
|
|
"crumhorn",
|
|
"sackbut",
|
|
"didgeridoo",
|
|
// Forms and pieces
|
|
"sonata",
|
|
"fugue",
|
|
"canon",
|
|
"prelude",
|
|
"overture",
|
|
"rondo",
|
|
"nocturne",
|
|
"scherzo",
|
|
"etude",
|
|
"ballade",
|
|
"barcarolle",
|
|
"bagatelle",
|
|
"caprice",
|
|
"chaconne",
|
|
"passacaglia",
|
|
"toccata",
|
|
"intermezzo",
|
|
"interlude",
|
|
"minuet",
|
|
"gavotte",
|
|
"sarabande",
|
|
"gigue",
|
|
"allemande",
|
|
"courante",
|
|
"mazurka",
|
|
"polonaise",
|
|
"waltz",
|
|
"bolero",
|
|
"habanera",
|
|
"tarantella",
|
|
"fandango",
|
|
"serenade",
|
|
"aubade",
|
|
"requiem",
|
|
"cantata",
|
|
"oratorio",
|
|
"madrigal",
|
|
"motet",
|
|
"chorale",
|
|
"hymn",
|
|
"anthem",
|
|
"lullaby",
|
|
"elegy",
|
|
"rhapsody",
|
|
"symphony",
|
|
"concerto",
|
|
"fantasia",
|
|
"divertimento",
|
|
"quartet",
|
|
"quintet",
|
|
"sextet",
|
|
"septet",
|
|
"octet",
|
|
"trio",
|
|
// Structure, notation and theory
|
|
"octave",
|
|
"chord",
|
|
"cadence",
|
|
"cadenza",
|
|
"coda",
|
|
"motif",
|
|
"refrain",
|
|
"verse",
|
|
"chorus",
|
|
"bridge",
|
|
"finale",
|
|
"arpeggio",
|
|
"glissando",
|
|
"ostinato",
|
|
"counterpoint",
|
|
"descant",
|
|
"harmony",
|
|
"melody",
|
|
"rhythm",
|
|
"tempo",
|
|
"timbre",
|
|
"treble",
|
|
"clef",
|
|
"staff",
|
|
"quaver",
|
|
"crotchet",
|
|
"semibreve",
|
|
"fermata",
|
|
"trill",
|
|
"mordent",
|
|
"appoggiatura",
|
|
"portamento",
|
|
"vibrato",
|
|
"triplet",
|
|
"tuplet",
|
|
"downbeat",
|
|
"upbeat",
|
|
"anacrusis",
|
|
"syncopation",
|
|
"modulation",
|
|
"transposition",
|
|
"unison",
|
|
"semitone",
|
|
"tritone",
|
|
"diapason",
|
|
"tonic",
|
|
"dominant",
|
|
"mediant",
|
|
"ionian",
|
|
"dorian",
|
|
"phrygian",
|
|
"lydian",
|
|
"mixolydian",
|
|
"aeolian",
|
|
"locrian",
|
|
"pentatonic",
|
|
"chromatic",
|
|
"diatonic",
|
|
// Tempo, dynamics and articulation
|
|
"allegro",
|
|
"andante",
|
|
"adagio",
|
|
"presto",
|
|
"vivace",
|
|
"largo",
|
|
"grave",
|
|
"lento",
|
|
"moderato",
|
|
"crescendo",
|
|
"diminuendo",
|
|
"staccato",
|
|
"legato",
|
|
"forte",
|
|
"dolce",
|
|
"agitato",
|
|
"cantabile",
|
|
"sostenuto",
|
|
"tremolo",
|
|
"rubato",
|
|
"marcato",
|
|
"brillante",
|
|
"maestoso",
|
|
"grazioso",
|
|
"espressivo",
|
|
"tranquillo",
|
|
"animato",
|
|
"spirito",
|
|
"sotto",
|
|
"vivo",
|
|
"feroce",
|
|
"giocoso",
|
|
"affettuoso",
|
|
"risoluto",
|
|
"leggiero",
|
|
"pesante",
|
|
"morendo",
|
|
"accelerando",
|
|
"ritardando",
|
|
];
|
|
|
|
/// Upper bound for a `GET /api/names?count=N` batch.
|
|
pub const MAX_BATCH: usize = 100;
|
|
|
|
/// A fresh one-word handle, e.g. `nocturne`.
|
|
pub fn generate() -> String {
|
|
use rand::seq::SliceRandom;
|
|
let mut rng = rand::thread_rng();
|
|
(*WORDS.choose(&mut rng).unwrap()).to_string()
|
|
}
|
|
|
|
/// `count` fresh handles, distinct while the wordlist lasts (the caller
|
|
/// clamps; this only trusts its input).
|
|
pub fn generate_batch(count: usize) -> Vec<String> {
|
|
use rand::seq::SliceRandom;
|
|
let mut rng = rand::thread_rng();
|
|
let mut pool: Vec<&str> = WORDS.to_vec();
|
|
pool.shuffle(&mut rng);
|
|
(0..count)
|
|
.map(|i| match pool.get(i) {
|
|
// Past the wordlist a batch has to repeat itself.
|
|
Some(word) => (*word).to_string(),
|
|
None => generate(),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::collections::HashSet;
|
|
|
|
#[test]
|
|
fn generated_name_is_a_single_known_word() {
|
|
let name = generate();
|
|
assert!(!name.contains('-'), "{name} is not one word");
|
|
assert!(WORDS.contains(&name.as_str()), "{name} not in WORDS");
|
|
}
|
|
|
|
#[test]
|
|
fn the_wordlist_has_no_duplicates_and_covers_a_full_batch() {
|
|
let unique: HashSet<&&str> = WORDS.iter().collect();
|
|
assert_eq!(unique.len(), WORDS.len(), "WORDS has duplicates");
|
|
assert!(
|
|
WORDS.len() >= MAX_BATCH,
|
|
"a full batch would have to repeat"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn batch_returns_the_requested_count_without_repeats() {
|
|
assert_eq!(generate_batch(1).len(), 1);
|
|
let batch = generate_batch(MAX_BATCH);
|
|
assert_eq!(batch.len(), MAX_BATCH);
|
|
for name in &batch {
|
|
assert!(WORDS.contains(&name.as_str()), "{name} not in WORDS");
|
|
}
|
|
let unique: HashSet<&String> = batch.iter().collect();
|
|
assert_eq!(unique.len(), batch.len(), "batch repeated a handle");
|
|
}
|
|
|
|
#[test]
|
|
fn rerolls_can_differ() {
|
|
let first = generate();
|
|
let differs = (0..20).any(|_| generate() != first);
|
|
assert!(differs);
|
|
}
|
|
}
|