Skip to main content

zellij_utils/
sessions.rs

1use crate::{
2    consts::{
3        is_ipc_socket, session_info_folder_for_session, session_layout_cache_file_name,
4        ZELLIJ_SESSION_INFO_CACHE_DIR, ZELLIJ_SOCK_DIR,
5    },
6    data::SessionInfo,
7    envs,
8    input::layout::Layout,
9    ipc::{ClientToServerMsg, IpcReceiverWithContext, IpcSenderWithContext, ServerToClientMsg},
10};
11use anyhow;
12use humantime::format_duration;
13use std::collections::{BTreeMap, HashMap};
14use std::path::Path;
15use std::time::{Duration, SystemTime};
16use std::{fs, io, process};
17use suggest::Suggest;
18
19pub fn get_sessions() -> Result<Vec<(String, Duration)>, io::ErrorKind> {
20    match fs::read_dir(&*ZELLIJ_SOCK_DIR) {
21        Ok(files) => {
22            let mut sessions = Vec::new();
23            files.for_each(|file| {
24                if let Ok(file) = file {
25                    let file_name = file.file_name().into_string().unwrap();
26                    // try to get creation time, fall back to modification time on platforms where it's not supported (e.g., musl)
27                    // for session creation time these are almost always identical (notable
28                    // exceptions are session name changes)
29                    let ctime = std::fs::metadata(&file.path())
30                        .ok()
31                        .and_then(|f| f.created().ok().or_else(|| f.modified().ok()))
32                        .and_then(|d| d.elapsed().ok())
33                        .unwrap_or_default();
34                    let duration = Duration::from_secs(ctime.as_secs());
35                    if is_ipc_socket(&file.file_type().unwrap()) && assert_socket(&file_name) {
36                        sessions.push((file_name, duration));
37                    }
38                }
39            });
40            Ok(sessions)
41        },
42        Err(err) if io::ErrorKind::NotFound != err.kind() => Err(err.kind()),
43        Err(_) => Ok(Vec::with_capacity(0)),
44    }
45}
46
47pub fn get_resurrectable_sessions() -> Vec<(String, Duration)> {
48    match fs::read_dir(&*ZELLIJ_SESSION_INFO_CACHE_DIR) {
49        Ok(files_in_session_info_folder) => {
50            let files_that_are_folders = files_in_session_info_folder
51                .filter_map(|f| f.ok().map(|f| f.path()))
52                .filter(|f| f.is_dir());
53            files_that_are_folders
54                .filter_map(|folder_name| {
55                    let layout_file_name =
56                        session_layout_cache_file_name(&folder_name.display().to_string());
57                    // Try to get creation time, fall back to modification time on platforms where it's not supported (e.g., musl)
58                    let ctime = std::fs::metadata(&layout_file_name)
59                        .ok()
60                        .and_then(|metadata| {
61                            metadata.created().ok().or_else(|| metadata.modified().ok())
62                        });
63                    let elapsed_duration = ctime
64                        .map(|ctime| {
65                            Duration::from_secs(ctime.elapsed().ok().unwrap_or_default().as_secs())
66                        })
67                        .unwrap_or_default();
68                    let session_name = folder_name
69                        .file_name()
70                        .map(|f| std::path::PathBuf::from(f).display().to_string())?;
71                    if std::path::Path::new(&layout_file_name).exists() {
72                        Some((session_name, elapsed_duration))
73                    } else {
74                        None
75                    }
76                })
77                .collect()
78        },
79        Err(e) => {
80            log::error!(
81                "Failed to read session_info cache folder: \"{:?}\": {:?}",
82                &*ZELLIJ_SESSION_INFO_CACHE_DIR,
83                e
84            );
85            vec![]
86        },
87    }
88}
89
90pub fn get_resurrectable_session_names() -> Vec<String> {
91    match fs::read_dir(&*ZELLIJ_SESSION_INFO_CACHE_DIR) {
92        Ok(files_in_session_info_folder) => {
93            let files_that_are_folders = files_in_session_info_folder
94                .filter_map(|f| f.ok().map(|f| f.path()))
95                .filter(|f| f.is_dir());
96            files_that_are_folders
97                .filter_map(|folder_name| {
98                    let folder = folder_name.display().to_string();
99                    let resurrection_layout_file = session_layout_cache_file_name(&folder);
100                    if std::path::Path::new(&resurrection_layout_file).exists() {
101                        folder_name
102                            .file_name()
103                            .map(|f| format!("{}", f.to_string_lossy()))
104                    } else {
105                        None
106                    }
107                })
108                .collect()
109        },
110        Err(e) => {
111            log::error!(
112                "Failed to read session_info cache folder: \"{:?}\": {:?}",
113                &*ZELLIJ_SESSION_INFO_CACHE_DIR,
114                e
115            );
116            vec![]
117        },
118    }
119}
120
121pub fn get_sessions_sorted_by_mtime() -> anyhow::Result<Vec<String>> {
122    match fs::read_dir(&*ZELLIJ_SOCK_DIR) {
123        Ok(files) => {
124            let mut sessions_with_mtime: Vec<(String, SystemTime)> = Vec::new();
125            for file in files {
126                let file = file?;
127                let file_name = file.file_name().into_string().unwrap();
128                let file_modified_at = file.metadata()?.modified()?;
129                if is_ipc_socket(&file.file_type()?) && assert_socket(&file_name) {
130                    sessions_with_mtime.push((file_name, file_modified_at));
131                }
132            }
133            sessions_with_mtime.sort_by_key(|x| x.1); // the oldest one will be the first
134
135            let sessions = sessions_with_mtime.iter().map(|x| x.0.clone()).collect();
136            Ok(sessions)
137        },
138        Err(err) if io::ErrorKind::NotFound != err.kind() => Err(err.into()),
139        Err(_) => Ok(Vec::with_capacity(0)),
140    }
141}
142
143/// Probe a session socket to check if a server is alive.
144///
145/// On Unix, connects and sends a `ConnStatus` message to verify the server responds.
146/// On Windows, reads the server PID from the marker file and checks process liveness.
147#[cfg(unix)]
148fn assert_socket(name: &str) -> bool {
149    use crate::consts::ipc_connect;
150    let path = &*ZELLIJ_SOCK_DIR.join(name);
151    match ipc_connect(path) {
152        Ok(stream) => {
153            let mut sender: IpcSenderWithContext<ClientToServerMsg> =
154                IpcSenderWithContext::new(stream);
155            let _ = sender.send_client_msg(ClientToServerMsg::ConnStatus);
156            let mut receiver: IpcReceiverWithContext<ServerToClientMsg> = sender.get_receiver();
157            match receiver.recv_server_msg() {
158                Some((ServerToClientMsg::Connected, _)) => true,
159                None | Some((_, _)) => false,
160            }
161        },
162        Err(e) if e.kind() == io::ErrorKind::ConnectionRefused => {
163            drop(fs::remove_file(path));
164            false
165        },
166        Err(_) => false,
167    }
168}
169
170/// On Windows, reads the server PID from the marker file and checks whether
171/// the process is still alive via `OpenProcess`. Cleans up stale marker files.
172#[cfg(windows)]
173fn assert_socket(name: &str) -> bool {
174    use windows_sys::Win32::Foundation::CloseHandle;
175    use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION};
176
177    let path = &*ZELLIJ_SOCK_DIR.join(name);
178    let pid_str = match fs::read_to_string(path) {
179        Ok(s) => s,
180        Err(_) => {
181            drop(fs::remove_file(path));
182            return false;
183        },
184    };
185    let pid: u32 = match pid_str.trim().parse() {
186        Ok(p) => p,
187        Err(_) => {
188            // Marker file exists but has no valid PID (e.g. empty from old version).
189            // Treat as stale.
190            drop(fs::remove_file(path));
191            return false;
192        },
193    };
194    let alive = unsafe {
195        let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
196        if handle.is_null() {
197            false
198        } else {
199            CloseHandle(handle);
200            true
201        }
202    };
203    if !alive {
204        drop(fs::remove_file(path));
205    }
206    alive
207}
208
209#[cfg(not(any(unix, windows)))]
210fn assert_socket(_name: &str) -> bool {
211    true
212}
213
214pub fn print_sessions(
215    mut sessions: Vec<(String, Duration, bool)>,
216    no_formatting: bool,
217    short: bool,
218    reverse: bool,
219) {
220    // (session_name, timestamp, is_dead)
221    let curr_session = envs::get_session_name().unwrap_or_else(|_| "".into());
222    sessions.sort_by(|a, b| {
223        if reverse {
224            // sort by `Duration` ascending (newest would be first)
225            a.1.cmp(&b.1)
226        } else {
227            b.1.cmp(&a.1)
228        }
229    });
230    sessions
231        .iter()
232        .for_each(|(session_name, timestamp, is_dead)| {
233            if short {
234                println!("{}", session_name);
235                return;
236            }
237            if no_formatting {
238                let suffix = if curr_session == *session_name {
239                    format!("(current)")
240                } else if *is_dead {
241                    format!("(EXITED - attach to resurrect)")
242                } else {
243                    String::new()
244                };
245                let timestamp = format!("[Created {} ago]", format_duration(*timestamp));
246                println!("{} {} {}", session_name, timestamp, suffix);
247            } else {
248                let formatted_session_name = format!("\u{1b}[32;1m{}\u{1b}[m", session_name);
249                let suffix = if curr_session == *session_name {
250                    format!("(current)")
251                } else if *is_dead {
252                    format!("(\u{1b}[31;1mEXITED\u{1b}[m - attach to resurrect)")
253                } else {
254                    String::new()
255                };
256                let timestamp = format!(
257                    "[Created \u{1b}[35;1m{}\u{1b}[m ago]",
258                    format_duration(*timestamp)
259                );
260                println!("{} {} {}", formatted_session_name, timestamp, suffix);
261            }
262        })
263}
264
265pub fn print_sessions_with_index(sessions: Vec<String>) {
266    let curr_session = envs::get_session_name().unwrap_or_else(|_| "".into());
267    for (i, session) in sessions.iter().enumerate() {
268        let suffix = if curr_session == *session {
269            " (current)"
270        } else {
271            ""
272        };
273        println!("{}: {}{}", i, session, suffix);
274    }
275}
276
277pub enum ActiveSession {
278    None,
279    One(String),
280    Many,
281}
282
283pub fn get_active_session() -> ActiveSession {
284    match get_sessions() {
285        Ok(sessions) if sessions.is_empty() => ActiveSession::None,
286        Ok(mut sessions) if sessions.len() == 1 => ActiveSession::One(sessions.pop().unwrap().0),
287        Ok(_) => ActiveSession::Many,
288        Err(e) => {
289            eprintln!("Error occurred: {:?}", e);
290            process::exit(1);
291        },
292    }
293}
294
295pub fn kill_session(name: &str) {
296    use crate::consts::ipc_connect;
297    let path = &*ZELLIJ_SOCK_DIR.join(name);
298    match ipc_connect(path) {
299        Ok(stream) => {
300            // On Windows, the server uses a dual-pipe architecture: the main pipe
301            // for client→server and a reply pipe for server→client. We must:
302            // 1. Connect to the reply pipe (so the server unblocks from
303            //    reply_listener.accept() and spawns the route thread)
304            // 2. Send KillSession on the main pipe
305            // 3. Wait for the Exit response on the reply pipe (so we don't
306            //    disconnect before the server processes the message)
307            #[cfg(windows)]
308            {
309                let reply = crate::consts::ipc_connect_reply(path);
310                let _ = IpcSenderWithContext::<ClientToServerMsg>::new(stream)
311                    .send_client_msg(ClientToServerMsg::KillSession);
312                if let Ok(reply_stream) = reply {
313                    let mut receiver: IpcReceiverWithContext<ServerToClientMsg> =
314                        IpcReceiverWithContext::new(reply_stream);
315                    let _ = receiver.recv_server_msg();
316                }
317            }
318            #[cfg(not(windows))]
319            {
320                let _ = IpcSenderWithContext::<ClientToServerMsg>::new(stream)
321                    .send_client_msg(ClientToServerMsg::KillSession);
322            }
323        },
324        Err(e) => {
325            eprintln!("Error occurred: {:?}", e);
326            process::exit(1);
327        },
328    };
329}
330
331pub fn delete_session(name: &str, force: bool) {
332    if force {
333        use crate::consts::ipc_connect;
334        let path = &*ZELLIJ_SOCK_DIR.join(name);
335        let _ = ipc_connect(path).ok().map(|stream| {
336            #[cfg(windows)]
337            {
338                let reply = crate::consts::ipc_connect_reply(path);
339                let _ = IpcSenderWithContext::<ClientToServerMsg>::new(stream)
340                    .send_client_msg(ClientToServerMsg::KillSession);
341                if let Ok(reply_stream) = reply {
342                    let mut receiver: IpcReceiverWithContext<ServerToClientMsg> =
343                        IpcReceiverWithContext::new(reply_stream);
344                    let _ = receiver.recv_server_msg();
345                }
346            }
347            #[cfg(not(windows))]
348            {
349                IpcSenderWithContext::<ClientToServerMsg>::new(stream)
350                    .send_client_msg(ClientToServerMsg::KillSession)
351                    .ok();
352            }
353        });
354    }
355    if let Err(e) = std::fs::remove_dir_all(session_info_folder_for_session(name)) {
356        if e.kind() == std::io::ErrorKind::NotFound {
357            eprintln!("Session: {:?} not found.", name);
358            process::exit(2);
359        } else {
360            log::error!("Failed to remove session {:?}: {:?}", name, e);
361        }
362    } else {
363        println!("Session: {:?} successfully deleted.", name);
364    }
365}
366
367pub fn list_sessions(no_formatting: bool, short: bool, reverse: bool) {
368    let exit_code = match get_sessions() {
369        Ok(running_sessions) => {
370            let resurrectable_sessions = get_resurrectable_sessions();
371            let mut all_sessions: HashMap<String, (Duration, bool)> = resurrectable_sessions
372                .iter()
373                .map(|(name, timestamp)| (name.clone(), (timestamp.clone(), true)))
374                .collect();
375            for (session_name, duration) in running_sessions {
376                all_sessions.insert(session_name.clone(), (duration, false));
377            }
378            if all_sessions.is_empty() {
379                eprintln!("No active zellij sessions found.");
380                1
381            } else {
382                print_sessions(
383                    all_sessions
384                        .iter()
385                        .map(|(name, (timestamp, is_dead))| {
386                            (name.clone(), timestamp.clone(), *is_dead)
387                        })
388                        .collect(),
389                    no_formatting,
390                    short,
391                    reverse,
392                );
393                0
394            }
395        },
396        Err(e) => {
397            eprintln!("Error occurred: {:?}", e);
398            1
399        },
400    };
401    process::exit(exit_code);
402}
403
404#[derive(Debug, Clone)]
405pub enum SessionNameMatch {
406    AmbiguousPrefix(Vec<String>),
407    UniquePrefix(String),
408    Exact(String),
409    None,
410}
411
412pub fn match_session_name(prefix: &str) -> Result<SessionNameMatch, io::ErrorKind> {
413    let sessions = get_sessions()?;
414
415    let filtered_sessions: Vec<_> = sessions
416        .iter()
417        .filter(|s| s.0.starts_with(prefix))
418        .collect();
419
420    if filtered_sessions.iter().any(|s| s.0 == prefix) {
421        return Ok(SessionNameMatch::Exact(prefix.to_string()));
422    }
423
424    Ok({
425        match &filtered_sessions[..] {
426            [] => SessionNameMatch::None,
427            [s] => SessionNameMatch::UniquePrefix(s.0.to_string()),
428            _ => SessionNameMatch::AmbiguousPrefix(
429                filtered_sessions.into_iter().map(|s| s.0.clone()).collect(),
430            ),
431        }
432    })
433}
434
435pub fn session_exists(name: &str) -> Result<bool, io::ErrorKind> {
436    match match_session_name(name) {
437        Ok(SessionNameMatch::Exact(_)) => Ok(true),
438        Ok(_) => Ok(false),
439        Err(e) => Err(e),
440    }
441}
442
443// if the session is resurrecable, the returned layout is the one to be used to resurrect it
444pub fn resurrection_layout(session_name_to_resurrect: &str) -> Result<Option<Layout>, String> {
445    let layout_file_name = session_layout_cache_file_name(&session_name_to_resurrect);
446    let raw_layout = match std::fs::read_to_string(&layout_file_name) {
447        Ok(raw_layout) => raw_layout,
448        Err(_e) => {
449            return Ok(None);
450        },
451    };
452    match Layout::from_kdl(
453        &raw_layout,
454        Some(layout_file_name.display().to_string()),
455        None,
456        None,
457    ) {
458        Ok(layout) => Ok(Some(layout)),
459        Err(e) => {
460            log::error!(
461                "Failed to parse resurrection layout file {}: {}",
462                layout_file_name.display(),
463                e
464            );
465            return Err(format!(
466                "Failed to parse resurrection layout file {}: {}.",
467                layout_file_name.display(),
468                e
469            ));
470        },
471    }
472}
473
474pub fn assert_session(name: &str) {
475    match session_exists(name) {
476        Ok(result) => {
477            if result {
478                return;
479            } else {
480                println!("No session named {:?} found.", name);
481                if let Some(sugg) = get_sessions()
482                    .unwrap()
483                    .iter()
484                    .map(|s| s.0.clone())
485                    .collect::<Vec<_>>()
486                    .suggest(name)
487                {
488                    println!("  help: Did you mean `{}`?", sugg);
489                }
490            }
491        },
492        Err(e) => {
493            eprintln!("Error occurred: {:?}", e);
494        },
495    };
496    process::exit(1);
497}
498
499pub fn assert_dead_session(name: &str, force: bool) {
500    match session_exists(name) {
501        Ok(exists) => {
502            if exists && !force {
503                println!(
504                    "A session by the name {:?} exists and is active, use --force to delete it.",
505                    name
506                )
507            } else if exists && force {
508                println!("A session by the name {:?} exists and is active, but will be force killed and deleted.", name);
509                return;
510            } else {
511                return;
512            }
513        },
514        Err(e) => {
515            eprintln!("Error occurred: {:?}", e);
516        },
517    };
518    process::exit(1);
519}
520
521pub fn validate_session_name(name: &str) -> Result<(), String> {
522    if name.trim().is_empty() {
523        return Err(
524            "Session name cannot be empty. Please provide a specific session name.".to_string(),
525        );
526    }
527    if name == "." || name == ".." {
528        return Err(format!("Invalid session name: \"{}\".", name));
529    }
530    if name.contains('/') {
531        return Err("Session name cannot contain '/'.".to_string());
532    }
533    Ok(())
534}
535
536pub fn assert_session_ne(name: &str) {
537    if let Err(e) = validate_session_name(name) {
538        eprintln!("{}", e);
539        process::exit(1);
540    }
541
542    match session_exists(name) {
543        Ok(result) if !result => {
544            let resurrectable_sessions = get_resurrectable_session_names();
545            if resurrectable_sessions.iter().find(|s| s == &name).is_some() {
546                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);
547            } else {
548                return
549            }
550        }
551        Ok(_) => println!("Session with name {:?} already exists. Use attach command to connect to it or specify a different name.", name),
552        Err(e) => eprintln!("Error occurred: {:?}", e),
553    };
554    process::exit(1);
555}
556
557pub fn session_listing_error_message(kind: io::ErrorKind) -> String {
558    format!(
559        "Failed to list existing Zellij sessions in the socket directory:\n  {}\n\n\
560         Reason: {}\n\n\
561         This usually means the directory (or one of its parents) is not readable \
562         by the current user - for example if $ZELLIJ_SOCKET_DIR or $XDG_RUNTIME_DIR \
563         points to a directory you do not own.\n\
564         To fix this, set a readable and writable socket directory, eg.:\n  \
565         ZELLIJ_SOCKET_DIR=/tmp/zellij-$USER zellij",
566        ZELLIJ_SOCK_DIR.display(),
567        io::Error::from(kind)
568    )
569}
570
571pub fn read_live_session_states(
572    current_session_name: &str,
573    sock_dir: &Path,
574    session_info_cache_dir: &Path,
575) -> BTreeMap<String, SessionInfo> {
576    let mut other_session_names: Vec<(String, Duration)> = vec![];
577    let mut session_infos_on_machine = BTreeMap::new();
578    if let Ok(files) = fs::read_dir(sock_dir) {
579        files.for_each(|file| {
580            if let Ok(file) = file {
581                if let Ok(file_name) = file.file_name().into_string() {
582                    if file
583                        .file_type()
584                        .map(|file_type| is_ipc_socket(&file_type))
585                        .unwrap_or(false)
586                    {
587                        let creation_time = std::fs::metadata(&file.path())
588                            .ok()
589                            .and_then(|f| f.created().ok().or_else(|| f.modified().ok()))
590                            .and_then(|d| d.elapsed().ok())
591                            .unwrap_or_default();
592                        other_session_names.push((file_name, creation_time));
593                    }
594                }
595            }
596        });
597    }
598
599    for (session_name, creation_time) in other_session_names {
600        let session_cache_file_name = session_info_cache_dir
601            .join(&session_name)
602            .join("session-metadata.kdl");
603        if let Ok(raw_session_info) = fs::read_to_string(&session_cache_file_name) {
604            if let Ok(mut session_info) =
605                SessionInfo::from_string(&raw_session_info, current_session_name)
606            {
607                session_info.creation_time = creation_time;
608                session_infos_on_machine.insert(session_name, session_info);
609            }
610        }
611    }
612    session_infos_on_machine
613}
614
615pub fn read_live_session_states_default_dirs(
616    current_session_name: &str,
617) -> BTreeMap<String, SessionInfo> {
618    read_live_session_states(
619        current_session_name,
620        &*ZELLIJ_SOCK_DIR,
621        &*ZELLIJ_SESSION_INFO_CACHE_DIR,
622    )
623}
624
625pub fn generate_unique_session_name() -> Option<String> {
626    let sessions = get_sessions().map(|sessions| {
627        sessions
628            .iter()
629            .map(|s| s.0.clone())
630            .collect::<Vec<String>>()
631    });
632    let dead_sessions = get_resurrectable_session_names();
633    let sessions = match sessions {
634        Ok(sessions) => sessions,
635        Err(kind) => {
636            eprintln!("{}", session_listing_error_message(kind));
637            return None;
638        },
639    };
640
641    let name = get_name_generator()
642        .take(1000)
643        .find(|name| !sessions.contains(name) && !dead_sessions.contains(name));
644
645    if let Some(name) = name {
646        return Some(name);
647    } else {
648        return None;
649    }
650}
651
652/// Create a new random name generator
653///
654/// Used to provide a memorable handle for a session when users don't specify a session name when the session is
655/// created.
656///
657/// Uses the list of adjectives and nouns defined below, with the intention of avoiding unfortunate
658/// and offensive combinations. Care should be taken when adding or removing to either list due to the birthday paradox/
659/// hash collisions, e.g. with 4096 unique names, the likelihood of a collision in 10 session names is 1%.
660pub fn get_name_generator() -> impl Iterator<Item = String> {
661    names::Generator::new(&ADJECTIVES, &NOUNS, names::Name::Plain)
662}
663
664/// Generates a random human-readable name using curated adjectives and nouns.
665/// Returns a single name in the format: AdjectiveNoun (e.g., "BraveRustacean")
666pub fn generate_random_name() -> String {
667    get_name_generator().next().unwrap()
668}
669
670const ADJECTIVES: &[&'static str] = &[
671    "adamant",
672    "adept",
673    "adventurous",
674    "arcadian",
675    "auspicious",
676    "awesome",
677    "blossoming",
678    "brave",
679    "charming",
680    "chatty",
681    "circular",
682    "considerate",
683    "cubic",
684    "curious",
685    "delighted",
686    "didactic",
687    "diligent",
688    "effulgent",
689    "erudite",
690    "excellent",
691    "exquisite",
692    "fabulous",
693    "fascinating",
694    "friendly",
695    "glowing",
696    "gracious",
697    "gregarious",
698    "hopeful",
699    "implacable",
700    "inventive",
701    "joyous",
702    "judicious",
703    "jumping",
704    "kind",
705    "likable",
706    "loyal",
707    "lucky",
708    "marvellous",
709    "mellifluous",
710    "nautical",
711    "oblong",
712    "outstanding",
713    "polished",
714    "polite",
715    "profound",
716    "quadratic",
717    "quiet",
718    "rectangular",
719    "remarkable",
720    "rusty",
721    "sensible",
722    "sincere",
723    "sparkling",
724    "splendid",
725    "stellar",
726    "tenacious",
727    "tremendous",
728    "triangular",
729    "undulating",
730    "unflappable",
731    "unique",
732    "verdant",
733    "vitreous",
734    "wise",
735    "zippy",
736];
737
738const NOUNS: &[&'static str] = &[
739    "aardvark",
740    "accordion",
741    "apple",
742    "apricot",
743    "bee",
744    "brachiosaur",
745    "cactus",
746    "capsicum",
747    "clarinet",
748    "cowbell",
749    "crab",
750    "cuckoo",
751    "cymbal",
752    "diplodocus",
753    "donkey",
754    "drum",
755    "duck",
756    "echidna",
757    "elephant",
758    "foxglove",
759    "galaxy",
760    "glockenspiel",
761    "goose",
762    "hill",
763    "horse",
764    "iguanodon",
765    "jellyfish",
766    "kangaroo",
767    "lake",
768    "lemon",
769    "lemur",
770    "magpie",
771    "megalodon",
772    "mountain",
773    "mouse",
774    "muskrat",
775    "newt",
776    "oboe",
777    "ocelot",
778    "orange",
779    "panda",
780    "peach",
781    "pepper",
782    "petunia",
783    "pheasant",
784    "piano",
785    "pigeon",
786    "platypus",
787    "quasar",
788    "rhinoceros",
789    "river",
790    "rustacean",
791    "salamander",
792    "sitar",
793    "stegosaurus",
794    "tambourine",
795    "tiger",
796    "tomato",
797    "triceratops",
798    "ukulele",
799    "viola",
800    "weasel",
801    "xylophone",
802    "yak",
803    "zebra",
804];