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 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 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); 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#[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#[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 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 let curr_session = envs::get_session_name().unwrap_or_else(|_| "".into());
222 sessions.sort_by(|a, b| {
223 if reverse {
224 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 #[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
443pub 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
652pub fn get_name_generator() -> impl Iterator<Item = String> {
661 names::Generator::new(&ADJECTIVES, &NOUNS, names::Name::Plain)
662}
663
664pub 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];