aboutsummaryrefslogtreecommitdiffstats
path: root/sl0ppij/zellij-utils/src/sessions.rs
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-12 19:34:15 +0200
committeruser <user@clank>2026-08-12 19:34:15 +0200
commitd1c14ff0f5c5b55c46d91ad12f4a291041480a99 (patch)
tree9dca9628d228b211a6919c9dd65cd7a9b4fcf6b0 /sl0ppij/zellij-utils/src/sessions.rs
parentchore: initialize sl0ppi monorepo (diff)
downloadsl0ppi-d1c14ff0f5c5b55c46d91ad12f4a291041480a99.tar.gz
sl0ppi-d1c14ff0f5c5b55c46d91ad12f4a291041480a99.tar.xz
sl0ppi-d1c14ff0f5c5b55c46d91ad12f4a291041480a99.zip
vendor(sl0ppij): import zellij v0.45.0 tree
Squashed import of upstream zellij at 0e6e4404 ("Support the Kitty Image Protocol (#5428)"), the merge-base our fork was developed against. Upstream's 3311 commits are deliberately NOT replayed here; only our own fork commits are, so the monorepo stays small and `git log sl0ppij/` shows sl0p work, not zellij's entire history. Upstream provenance is tracked in FORKS.md and can always be re-fetched from https://github.com/zellij-org/zellij.
Diffstat (limited to 'sl0ppij/zellij-utils/src/sessions.rs')
-rw-r--r--sl0ppij/zellij-utils/src/sessions.rs748
1 files changed, 748 insertions, 0 deletions
diff --git a/sl0ppij/zellij-utils/src/sessions.rs b/sl0ppij/zellij-utils/src/sessions.rs
new file mode 100644
index 0000000..7e42077
--- /dev/null
+++ b/sl0ppij/zellij-utils/src/sessions.rs
@@ -0,0 +1,748 @@
+use crate::{
+ consts::{
+ is_ipc_socket, session_info_folder_for_session, session_layout_cache_file_name,
+ ZELLIJ_SESSION_INFO_CACHE_DIR, ZELLIJ_SOCK_DIR,
+ },
+ envs,
+ input::layout::Layout,
+ ipc::{ClientToServerMsg, IpcReceiverWithContext, IpcSenderWithContext, ServerToClientMsg},
+};
+use anyhow;
+use humantime::format_duration;
+use std::collections::HashMap;
+use std::time::{Duration, SystemTime};
+use std::{fs, io, process};
+use suggest::Suggest;
+
+pub fn get_sessions() -> Result<Vec<(String, Duration)>, io::ErrorKind> {
+ match fs::read_dir(&*ZELLIJ_SOCK_DIR) {
+ Ok(files) => {
+ let mut sessions = Vec::new();
+ files.for_each(|file| {
+ if let Ok(file) = file {
+ let file_name = file.file_name().into_string().unwrap();
+ // try to get creation time, fall back to modification time on platforms where it's not supported (e.g., musl)
+ // for session creation time these are almost always identical (notable
+ // exceptions are session name changes)
+ let ctime = std::fs::metadata(&file.path())
+ .ok()
+ .and_then(|f| f.created().ok().or_else(|| f.modified().ok()))
+ .and_then(|d| d.elapsed().ok())
+ .unwrap_or_default();
+ let duration = Duration::from_secs(ctime.as_secs());
+ if is_ipc_socket(&file.file_type().unwrap()) && assert_socket(&file_name) {
+ sessions.push((file_name, duration));
+ }
+ }
+ });
+ Ok(sessions)
+ },
+ Err(err) if io::ErrorKind::NotFound != err.kind() => Err(err.kind()),
+ Err(_) => Ok(Vec::with_capacity(0)),
+ }
+}
+
+pub fn get_resurrectable_sessions() -> Vec<(String, Duration)> {
+ match fs::read_dir(&*ZELLIJ_SESSION_INFO_CACHE_DIR) {
+ Ok(files_in_session_info_folder) => {
+ let files_that_are_folders = files_in_session_info_folder
+ .filter_map(|f| f.ok().map(|f| f.path()))
+ .filter(|f| f.is_dir());
+ files_that_are_folders
+ .filter_map(|folder_name| {
+ let layout_file_name =
+ session_layout_cache_file_name(&folder_name.display().to_string());
+ // Try to get creation time, fall back to modification time on platforms where it's not supported (e.g., musl)
+ let ctime = std::fs::metadata(&layout_file_name)
+ .ok()
+ .and_then(|metadata| {
+ metadata.created().ok().or_else(|| metadata.modified().ok())
+ });
+ let elapsed_duration = ctime
+ .map(|ctime| {
+ Duration::from_secs(ctime.elapsed().ok().unwrap_or_default().as_secs())
+ })
+ .unwrap_or_default();
+ let session_name = folder_name
+ .file_name()
+ .map(|f| std::path::PathBuf::from(f).display().to_string())?;
+ if std::path::Path::new(&layout_file_name).exists() {
+ Some((session_name, elapsed_duration))
+ } else {
+ None
+ }
+ })
+ .collect()
+ },
+ Err(e) => {
+ log::error!(
+ "Failed to read session_info cache folder: \"{:?}\": {:?}",
+ &*ZELLIJ_SESSION_INFO_CACHE_DIR,
+ e
+ );
+ vec![]
+ },
+ }
+}
+
+pub fn get_resurrectable_session_names() -> Vec<String> {
+ match fs::read_dir(&*ZELLIJ_SESSION_INFO_CACHE_DIR) {
+ Ok(files_in_session_info_folder) => {
+ let files_that_are_folders = files_in_session_info_folder
+ .filter_map(|f| f.ok().map(|f| f.path()))
+ .filter(|f| f.is_dir());
+ files_that_are_folders
+ .filter_map(|folder_name| {
+ let folder = folder_name.display().to_string();
+ let resurrection_layout_file = session_layout_cache_file_name(&folder);
+ if std::path::Path::new(&resurrection_layout_file).exists() {
+ folder_name
+ .file_name()
+ .map(|f| format!("{}", f.to_string_lossy()))
+ } else {
+ None
+ }
+ })
+ .collect()
+ },
+ Err(e) => {
+ log::error!(
+ "Failed to read session_info cache folder: \"{:?}\": {:?}",
+ &*ZELLIJ_SESSION_INFO_CACHE_DIR,
+ e
+ );
+ vec![]
+ },
+ }
+}
+
+pub fn get_sessions_sorted_by_mtime() -> anyhow::Result<Vec<String>> {
+ match fs::read_dir(&*ZELLIJ_SOCK_DIR) {
+ Ok(files) => {
+ let mut sessions_with_mtime: Vec<(String, SystemTime)> = Vec::new();
+ for file in files {
+ let file = file?;
+ let file_name = file.file_name().into_string().unwrap();
+ let file_modified_at = file.metadata()?.modified()?;
+ if is_ipc_socket(&file.file_type()?) && assert_socket(&file_name) {
+ sessions_with_mtime.push((file_name, file_modified_at));
+ }
+ }
+ sessions_with_mtime.sort_by_key(|x| x.1); // the oldest one will be the first
+
+ let sessions = sessions_with_mtime.iter().map(|x| x.0.clone()).collect();
+ Ok(sessions)
+ },
+ Err(err) if io::ErrorKind::NotFound != err.kind() => Err(err.into()),
+ Err(_) => Ok(Vec::with_capacity(0)),
+ }
+}
+
+/// Probe a session socket to check if a server is alive.
+///
+/// On Unix, connects and sends a `ConnStatus` message to verify the server responds.
+/// On Windows, reads the server PID from the marker file and checks process liveness.
+#[cfg(unix)]
+fn assert_socket(name: &str) -> bool {
+ use crate::consts::ipc_connect;
+ let path = &*ZELLIJ_SOCK_DIR.join(name);
+ match ipc_connect(path) {
+ Ok(stream) => {
+ let mut sender: IpcSenderWithContext<ClientToServerMsg> =
+ IpcSenderWithContext::new(stream);
+ let _ = sender.send_client_msg(ClientToServerMsg::ConnStatus);
+ let mut receiver: IpcReceiverWithContext<ServerToClientMsg> = sender.get_receiver();
+ match receiver.recv_server_msg() {
+ Some((ServerToClientMsg::Connected, _)) => true,
+ None | Some((_, _)) => false,
+ }
+ },
+ Err(e) if e.kind() == io::ErrorKind::ConnectionRefused => {
+ drop(fs::remove_file(path));
+ false
+ },
+ Err(_) => false,
+ }
+}
+
+/// On Windows, reads the server PID from the marker file and checks whether
+/// the process is still alive via `OpenProcess`. Cleans up stale marker files.
+#[cfg(windows)]
+fn assert_socket(name: &str) -> bool {
+ use windows_sys::Win32::Foundation::CloseHandle;
+ use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION};
+
+ let path = &*ZELLIJ_SOCK_DIR.join(name);
+ let pid_str = match fs::read_to_string(path) {
+ Ok(s) => s,
+ Err(_) => {
+ drop(fs::remove_file(path));
+ return false;
+ },
+ };
+ let pid: u32 = match pid_str.trim().parse() {
+ Ok(p) => p,
+ Err(_) => {
+ // Marker file exists but has no valid PID (e.g. empty from old version).
+ // Treat as stale.
+ drop(fs::remove_file(path));
+ return false;
+ },
+ };
+ let alive = unsafe {
+ let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
+ if handle.is_null() {
+ false
+ } else {
+ CloseHandle(handle);
+ true
+ }
+ };
+ if !alive {
+ drop(fs::remove_file(path));
+ }
+ alive
+}
+
+#[cfg(not(any(unix, windows)))]
+fn assert_socket(_name: &str) -> bool {
+ true
+}
+
+pub fn print_sessions(
+ mut sessions: Vec<(String, Duration, bool)>,
+ no_formatting: bool,
+ short: bool,
+ reverse: bool,
+) {
+ // (session_name, timestamp, is_dead)
+ let curr_session = envs::get_session_name().unwrap_or_else(|_| "".into());
+ sessions.sort_by(|a, b| {
+ if reverse {
+ // sort by `Duration` ascending (newest would be first)
+ a.1.cmp(&b.1)
+ } else {
+ b.1.cmp(&a.1)
+ }
+ });
+ sessions
+ .iter()
+ .for_each(|(session_name, timestamp, is_dead)| {
+ if short {
+ println!("{}", session_name);
+ return;
+ }
+ if no_formatting {
+ let suffix = if curr_session == *session_name {
+ format!("(current)")
+ } else if *is_dead {
+ format!("(EXITED - attach to resurrect)")
+ } else {
+ String::new()
+ };
+ let timestamp = format!("[Created {} ago]", format_duration(*timestamp));
+ println!("{} {} {}", session_name, timestamp, suffix);
+ } else {
+ let formatted_session_name = format!("\u{1b}[32;1m{}\u{1b}[m", session_name);
+ let suffix = if curr_session == *session_name {
+ format!("(current)")
+ } else if *is_dead {
+ format!("(\u{1b}[31;1mEXITED\u{1b}[m - attach to resurrect)")
+ } else {
+ String::new()
+ };
+ let timestamp = format!(
+ "[Created \u{1b}[35;1m{}\u{1b}[m ago]",
+ format_duration(*timestamp)
+ );
+ println!("{} {} {}", formatted_session_name, timestamp, suffix);
+ }
+ })
+}
+
+pub fn print_sessions_with_index(sessions: Vec<String>) {
+ let curr_session = envs::get_session_name().unwrap_or_else(|_| "".into());
+ for (i, session) in sessions.iter().enumerate() {
+ let suffix = if curr_session == *session {
+ " (current)"
+ } else {
+ ""
+ };
+ println!("{}: {}{}", i, session, suffix);
+ }
+}
+
+pub enum ActiveSession {
+ None,
+ One(String),
+ Many,
+}
+
+pub fn get_active_session() -> ActiveSession {
+ match get_sessions() {
+ Ok(sessions) if sessions.is_empty() => ActiveSession::None,
+ Ok(mut sessions) if sessions.len() == 1 => ActiveSession::One(sessions.pop().unwrap().0),
+ Ok(_) => ActiveSession::Many,
+ Err(e) => {
+ eprintln!("Error occurred: {:?}", e);
+ process::exit(1);
+ },
+ }
+}
+
+pub fn kill_session(name: &str) {
+ use crate::consts::ipc_connect;
+ let path = &*ZELLIJ_SOCK_DIR.join(name);
+ match ipc_connect(path) {
+ Ok(stream) => {
+ // On Windows, the server uses a dual-pipe architecture: the main pipe
+ // for client→server and a reply pipe for server→client. We must:
+ // 1. Connect to the reply pipe (so the server unblocks from
+ // reply_listener.accept() and spawns the route thread)
+ // 2. Send KillSession on the main pipe
+ // 3. Wait for the Exit response on the reply pipe (so we don't
+ // disconnect before the server processes the message)
+ #[cfg(windows)]
+ {
+ let reply = crate::consts::ipc_connect_reply(path);
+ let _ = IpcSenderWithContext::<ClientToServerMsg>::new(stream)
+ .send_client_msg(ClientToServerMsg::KillSession);
+ if let Ok(reply_stream) = reply {
+ let mut receiver: IpcReceiverWithContext<ServerToClientMsg> =
+ IpcReceiverWithContext::new(reply_stream);
+ let _ = receiver.recv_server_msg();
+ }
+ }
+ #[cfg(not(windows))]
+ {
+ let _ = IpcSenderWithContext::<ClientToServerMsg>::new(stream)
+ .send_client_msg(ClientToServerMsg::KillSession);
+ }
+ },
+ Err(e) => {
+ eprintln!("Error occurred: {:?}", e);
+ process::exit(1);
+ },
+ };
+}
+
+pub fn delete_session(name: &str, force: bool) {
+ if force {
+ use crate::consts::ipc_connect;
+ let path = &*ZELLIJ_SOCK_DIR.join(name);
+ let _ = ipc_connect(path).ok().map(|stream| {
+ #[cfg(windows)]
+ {
+ let reply = crate::consts::ipc_connect_reply(path);
+ let _ = IpcSenderWithContext::<ClientToServerMsg>::new(stream)
+ .send_client_msg(ClientToServerMsg::KillSession);
+ if let Ok(reply_stream) = reply {
+ let mut receiver: IpcReceiverWithContext<ServerToClientMsg> =
+ IpcReceiverWithContext::new(reply_stream);
+ let _ = receiver.recv_server_msg();
+ }
+ }
+ #[cfg(not(windows))]
+ {
+ IpcSenderWithContext::<ClientToServerMsg>::new(stream)
+ .send_client_msg(ClientToServerMsg::KillSession)
+ .ok();
+ }
+ });
+ }
+ if let Err(e) = std::fs::remove_dir_all(session_info_folder_for_session(name)) {
+ if e.kind() == std::io::ErrorKind::NotFound {
+ eprintln!("Session: {:?} not found.", name);
+ process::exit(2);
+ } else {
+ log::error!("Failed to remove session {:?}: {:?}", name, e);
+ }
+ } else {
+ println!("Session: {:?} successfully deleted.", name);
+ }
+}
+
+pub fn list_sessions(no_formatting: bool, short: bool, reverse: bool) {
+ let exit_code = match get_sessions() {
+ Ok(running_sessions) => {
+ let resurrectable_sessions = get_resurrectable_sessions();
+ let mut all_sessions: HashMap<String, (Duration, bool)> = resurrectable_sessions
+ .iter()
+ .map(|(name, timestamp)| (name.clone(), (timestamp.clone(), true)))
+ .collect();
+ for (session_name, duration) in running_sessions {
+ all_sessions.insert(session_name.clone(), (duration, false));
+ }
+ if all_sessions.is_empty() {
+ eprintln!("No active zellij sessions found.");
+ 1
+ } else {
+ print_sessions(
+ all_sessions
+ .iter()
+ .map(|(name, (timestamp, is_dead))| {
+ (name.clone(), timestamp.clone(), *is_dead)
+ })
+ .collect(),
+ no_formatting,
+ short,
+ reverse,
+ );
+ 0
+ }
+ },
+ Err(e) => {
+ eprintln!("Error occurred: {:?}", e);
+ 1
+ },
+ };
+ process::exit(exit_code);
+}
+
+#[derive(Debug, Clone)]
+pub enum SessionNameMatch {
+ AmbiguousPrefix(Vec<String>),
+ UniquePrefix(String),
+ Exact(String),
+ None,
+}
+
+pub fn match_session_name(prefix: &str) -> Result<SessionNameMatch, io::ErrorKind> {
+ let sessions = get_sessions()?;
+
+ let filtered_sessions: Vec<_> = sessions
+ .iter()
+ .filter(|s| s.0.starts_with(prefix))
+ .collect();
+
+ if filtered_sessions.iter().any(|s| s.0 == prefix) {
+ return Ok(SessionNameMatch::Exact(prefix.to_string()));
+ }
+
+ Ok({
+ match &filtered_sessions[..] {
+ [] => SessionNameMatch::None,
+ [s] => SessionNameMatch::UniquePrefix(s.0.to_string()),
+ _ => SessionNameMatch::AmbiguousPrefix(
+ filtered_sessions.into_iter().map(|s| s.0.clone()).collect(),
+ ),
+ }
+ })
+}
+
+pub fn session_exists(name: &str) -> Result<bool, io::ErrorKind> {
+ match match_session_name(name) {
+ Ok(SessionNameMatch::Exact(_)) => Ok(true),
+ Ok(_) => Ok(false),
+ Err(e) => Err(e),
+ }
+}
+
+// if the session is resurrecable, the returned layout is the one to be used to resurrect it
+pub fn resurrection_layout(session_name_to_resurrect: &str) -> Result<Option<Layout>, String> {
+ let layout_file_name = session_layout_cache_file_name(&session_name_to_resurrect);
+ let raw_layout = match std::fs::read_to_string(&layout_file_name) {
+ Ok(raw_layout) => raw_layout,
+ Err(_e) => {
+ return Ok(None);
+ },
+ };
+ match Layout::from_kdl(
+ &raw_layout,
+ Some(layout_file_name.display().to_string()),
+ None,
+ None,
+ ) {
+ Ok(layout) => Ok(Some(layout)),
+ Err(e) => {
+ log::error!(
+ "Failed to parse resurrection layout file {}: {}",
+ layout_file_name.display(),
+ e
+ );
+ return Err(format!(
+ "Failed to parse resurrection layout file {}: {}.",
+ layout_file_name.display(),
+ e
+ ));
+ },
+ }
+}
+
+pub fn assert_session(name: &str) {
+ match session_exists(name) {
+ Ok(result) => {
+ if result {
+ return;
+ } else {
+ println!("No session named {:?} found.", name);
+ if let Some(sugg) = get_sessions()
+ .unwrap()
+ .iter()
+ .map(|s| s.0.clone())
+ .collect::<Vec<_>>()
+ .suggest(name)
+ {
+ println!(" help: Did you mean `{}`?", sugg);
+ }
+ }
+ },
+ Err(e) => {
+ eprintln!("Error occurred: {:?}", e);
+ },
+ };
+ process::exit(1);
+}
+
+pub fn assert_dead_session(name: &str, force: bool) {
+ match session_exists(name) {
+ Ok(exists) => {
+ if exists && !force {
+ println!(
+ "A session by the name {:?} exists and is active, use --force to delete it.",
+ name
+ )
+ } else if exists && force {
+ println!("A session by the name {:?} exists and is active, but will be force killed and deleted.", name);
+ return;
+ } else {
+ return;
+ }
+ },
+ Err(e) => {
+ eprintln!("Error occurred: {:?}", e);
+ },
+ };
+ process::exit(1);
+}
+
+pub fn validate_session_name(name: &str) -> Result<(), String> {
+ if name.trim().is_empty() {
+ return Err(
+ "Session name cannot be empty. Please provide a specific session name.".to_string(),
+ );
+ }
+ if name == "." || name == ".." {
+ return Err(format!("Invalid session name: \"{}\".", name));
+ }
+ if name.contains('/') {
+ return Err("Session name cannot contain '/'.".to_string());
+ }
+ Ok(())
+}
+
+pub fn assert_session_ne(name: &str) {
+ if let Err(e) = validate_session_name(name) {
+ eprintln!("{}", e);
+ process::exit(1);
+ }
+
+ match session_exists(name) {
+ Ok(result) if !result => {
+ let resurrectable_sessions = get_resurrectable_session_names();
+ if resurrectable_sessions.iter().find(|s| s == &name).is_some() {
+ println!("Session with name {:?} already exists, but is dead. Use the attach command to resurrect it or, the delete-session command to kill it or specify a different name.", name);
+ } else {
+ return
+ }
+ }
+ Ok(_) => println!("Session with name {:?} already exists. Use attach command to connect to it or specify a different name.", name),
+ Err(e) => eprintln!("Error occurred: {:?}", e),
+ };
+ process::exit(1);
+}
+
+pub fn session_listing_error_message(kind: io::ErrorKind) -> String {
+ format!(
+ "Failed to list existing Zellij sessions in the socket directory:\n {}\n\n\
+ Reason: {}\n\n\
+ This usually means the directory (or one of its parents) is not readable \
+ by the current user - for example if $ZELLIJ_SOCKET_DIR or $XDG_RUNTIME_DIR \
+ points to a directory you do not own.\n\
+ To fix this, set a readable and writable socket directory, eg.:\n \
+ ZELLIJ_SOCKET_DIR=/tmp/zellij-$USER zellij",
+ ZELLIJ_SOCK_DIR.display(),
+ io::Error::from(kind)
+ )
+}
+
+pub fn generate_unique_session_name() -> Option<String> {
+ let sessions = get_sessions().map(|sessions| {
+ sessions
+ .iter()
+ .map(|s| s.0.clone())
+ .collect::<Vec<String>>()
+ });
+ let dead_sessions = get_resurrectable_session_names();
+ let sessions = match sessions {
+ Ok(sessions) => sessions,
+ Err(kind) => {
+ eprintln!("{}", session_listing_error_message(kind));
+ return None;
+ },
+ };
+
+ let name = get_name_generator()
+ .take(1000)
+ .find(|name| !sessions.contains(name) && !dead_sessions.contains(name));
+
+ if let Some(name) = name {
+ return Some(name);
+ } else {
+ return None;
+ }
+}
+
+/// Create a new random name generator
+///
+/// Used to provide a memorable handle for a session when users don't specify a session name when the session is
+/// created.
+///
+/// Uses the list of adjectives and nouns defined below, with the intention of avoiding unfortunate
+/// and offensive combinations. Care should be taken when adding or removing to either list due to the birthday paradox/
+/// hash collisions, e.g. with 4096 unique names, the likelihood of a collision in 10 session names is 1%.
+pub fn get_name_generator() -> impl Iterator<Item = String> {
+ names::Generator::new(&ADJECTIVES, &NOUNS, names::Name::Plain)
+}
+
+/// Generates a random human-readable name using curated adjectives and nouns.
+/// Returns a single name in the format: AdjectiveNoun (e.g., "BraveRustacean")
+pub fn generate_random_name() -> String {
+ get_name_generator().next().unwrap()
+}
+
+const ADJECTIVES: &[&'static str] = &[
+ "adamant",
+ "adept",
+ "adventurous",
+ "arcadian",
+ "auspicious",
+ "awesome",
+ "blossoming",
+ "brave",
+ "charming",
+ "chatty",
+ "circular",
+ "considerate",
+ "cubic",
+ "curious",
+ "delighted",
+ "didactic",
+ "diligent",
+ "effulgent",
+ "erudite",
+ "excellent",
+ "exquisite",
+ "fabulous",
+ "fascinating",
+ "friendly",
+ "glowing",
+ "gracious",
+ "gregarious",
+ "hopeful",
+ "implacable",
+ "inventive",
+ "joyous",
+ "judicious",
+ "jumping",
+ "kind",
+ "likable",
+ "loyal",
+ "lucky",
+ "marvellous",
+ "mellifluous",
+ "nautical",
+ "oblong",
+ "outstanding",
+ "polished",
+ "polite",
+ "profound",
+ "quadratic",
+ "quiet",
+ "rectangular",
+ "remarkable",
+ "rusty",
+ "sensible",
+ "sincere",
+ "sparkling",
+ "splendid",
+ "stellar",
+ "tenacious",
+ "tremendous",
+ "triangular",
+ "undulating",
+ "unflappable",
+ "unique",
+ "verdant",
+ "vitreous",
+ "wise",
+ "zippy",
+];
+
+const NOUNS: &[&'static str] = &[
+ "aardvark",
+ "accordion",
+ "apple",
+ "apricot",
+ "bee",
+ "brachiosaur",
+ "cactus",
+ "capsicum",
+ "clarinet",
+ "cowbell",
+ "crab",
+ "cuckoo",
+ "cymbal",
+ "diplodocus",
+ "donkey",
+ "drum",
+ "duck",
+ "echidna",
+ "elephant",
+ "foxglove",
+ "galaxy",
+ "glockenspiel",
+ "goose",
+ "hill",
+ "horse",
+ "iguanadon",
+ "jellyfish",
+ "kangaroo",
+ "lake",
+ "lemon",
+ "lemur",
+ "magpie",
+ "megalodon",
+ "mountain",
+ "mouse",
+ "muskrat",
+ "newt",
+ "oboe",
+ "ocelot",
+ "orange",
+ "panda",
+ "peach",
+ "pepper",
+ "petunia",
+ "pheasant",
+ "piano",
+ "pigeon",
+ "platypus",
+ "quasar",
+ "rhinoceros",
+ "river",
+ "rustacean",
+ "salamander",
+ "sitar",
+ "stegosaurus",
+ "tambourine",
+ "tiger",
+ "tomato",
+ "triceratops",
+ "ukulele",
+ "viola",
+ "weasel",
+ "xylophone",
+ "yak",
+ "zebra",
+];