1use std::fmt;
4
5use muxr_core::SessionName;
6use owo_colors::OwoColorize;
7use rootcause::report;
8use strum::EnumIter;
9use strum::IntoEnumIterator;
10
11#[derive(Clone, Copy, Debug, EnumIter, Eq, PartialEq)]
12enum SessionAction {
13 Attach,
14 Delete,
15}
16
17impl fmt::Display for SessionAction {
18 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19 match self {
20 Self::Attach => write!(f, "{}", "Attach".green().bold()),
21 Self::Delete => write!(f, "{}", "Delete".red().bold()),
22 }
23 }
24}
25
26pub fn run() -> rootcause::Result<()> {
27 let sessions = muxr_client::list_sessions()?;
28 if sessions.is_empty() {
29 super::start::run(&SessionName::default(), None)?;
30 return Ok(());
31 }
32
33 let Some(selected) = ytil_tui::minimal_multi_select(
34 sessions,
35 muxr_client::ListedSession::display_text,
36 muxr_client::ListedSession::search_text,
37 )?
38 else {
39 println!("No sessions selected");
40 return Ok(());
41 };
42
43 let Some(action) = ytil_tui::minimal_select::<SessionAction>(SessionAction::iter().collect())? else {
44 println!("No action selected");
45 return Ok(());
46 };
47
48 let sessions = selected
49 .iter()
50 .map(|session| session.name().clone())
51 .collect::<Vec<_>>();
52 self::execute_session_action(action, &sessions)
53}
54
55fn execute_session_action(action: SessionAction, selected: &[SessionName]) -> rootcause::Result<()> {
56 match action {
57 SessionAction::Attach => {
58 let session = ytil_tui::require_single(selected, "sessions")?;
59 super::start::run(session, None)
60 }
61 SessionAction::Delete => self::delete_selected_sessions(selected, muxr_client::delete_session),
62 }
63}
64
65fn delete_selected_sessions<F>(selected: &[SessionName], mut delete_session: F) -> rootcause::Result<()>
66where
67 F: FnMut(&SessionName) -> rootcause::Result<muxr_client::SessionDeleteOutcome>,
68{
69 let mut failures = Vec::new();
70
71 for session in selected {
72 match delete_session(session) {
73 Ok(outcome) => println!("{}", self::delete_session_message(session, outcome)),
74 Err(error) => {
75 eprintln!("{}", self::delete_session_failure_message(session, &error));
77 failures.push(format!("{session}: {error}"));
78 }
79 }
80 }
81
82 if failures.is_empty() {
83 Ok(())
84 } else {
85 Err(report!("failed to delete selected muxr sessions").attach(failures.join("\n")))
86 }
87}
88
89fn delete_session_message(session: &SessionName, outcome: muxr_client::SessionDeleteOutcome) -> String {
90 let deleted = format!("{}", "Deleted".red().bold());
91 match outcome {
92 muxr_client::SessionDeleteOutcome::LiveDeleted => {
93 format!("{deleted} {session}; stopped live server and removed state")
94 }
95 muxr_client::SessionDeleteOutcome::LiveVanishedForced => {
96 format!("{deleted} {session}; live server vanished, force-removed selected session files")
97 }
98 muxr_client::SessionDeleteOutcome::StoppedRemoved => {
99 format!("{deleted} {session}; removed stopped session state")
100 }
101 muxr_client::SessionDeleteOutcome::UnknownForced => {
102 format!("{deleted} {session}; force-removed unknown session files")
103 }
104 }
105}
106
107fn delete_session_failure_message(session: &SessionName, error: impl fmt::Display) -> String {
108 let failed = format!("{}", "Failed".red().bold());
109 format!("{failed} to delete session {session}: {error}")
110}
111
112#[cfg(test)]
113mod tests {
114 use rstest::rstest;
115 use test_that::prelude::*;
116
117 use super::*;
118
119 #[rstest]
120 #[case::attach(SessionAction::Attach, format!("{}", "Attach".green().bold()))]
121 #[case::delete(SessionAction::Delete, format!("{}", "Delete".red().bold()))]
122 fn test_session_action_display_when_action_varies_matches_zj_style(
123 #[case] action: SessionAction,
124 #[case] expected: String,
125 ) {
126 assert_that!(action.to_string(), eq(expected));
127 }
128
129 #[test]
130 fn test_delete_selected_sessions_when_one_delete_fails_still_attempts_all() -> rootcause::Result<()> {
131 let selected = ["ok", "bad", "later"]
132 .into_iter()
133 .map(str::parse)
134 .collect::<rootcause::Result<Vec<SessionName>>>()?;
135 let mut attempted = Vec::new();
136
137 let result = delete_selected_sessions(&selected, |session| {
138 attempted.push(session.to_string());
139 if session.as_ref() == "bad" {
140 Err(report!("delete failed"))
141 } else {
142 Ok(muxr_client::SessionDeleteOutcome::StoppedRemoved)
143 }
144 });
145
146 assert_that!(result, err(anything()));
147 assert_that!(attempted, eq(vec!["ok", "bad", "later"]));
148 Ok(())
149 }
150
151 #[test]
152 fn test_execute_session_action_when_attach_has_multiple_sessions_returns_error() -> rootcause::Result<()> {
153 let sessions = vec![listed_session("work")?, listed_session("notes")?];
154
155 let error = execute_session_action(SessionAction::Attach, &sessions)
156 .expect_err("expected attach multi-selection error");
157
158 assert_that!(error.to_string(), contains_substring("expected exactly one selection"));
159 Ok(())
160 }
161
162 #[rstest]
163 #[case::live_deleted(
164 muxr_client::SessionDeleteOutcome::LiveDeleted,
165 format!("{} work; stopped live server and removed state", "Deleted".red().bold())
166 )]
167 #[case::live_vanished_forced(
168 muxr_client::SessionDeleteOutcome::LiveVanishedForced,
169 format!("{} work; live server vanished, force-removed selected session files", "Deleted".red().bold())
170 )]
171 #[case::stopped_removed(
172 muxr_client::SessionDeleteOutcome::StoppedRemoved,
173 format!("{} work; removed stopped session state", "Deleted".red().bold())
174 )]
175 #[case::unknown_forced(
176 muxr_client::SessionDeleteOutcome::UnknownForced,
177 format!("{} work; force-removed unknown session files", "Deleted".red().bold())
178 )]
179 fn test_delete_session_message_when_outcome_varies_reports_behavior(
180 #[case] outcome: muxr_client::SessionDeleteOutcome,
181 #[case] expected: String,
182 ) -> rootcause::Result<()> {
183 assert_that!(delete_session_message(&listed_session("work")?, outcome), eq(expected));
184 Ok(())
185 }
186
187 #[test]
188 fn test_delete_session_failure_message_colors_failure_prefix() -> rootcause::Result<()> {
189 let error = report!("delete failed");
190 let message = delete_session_failure_message(&listed_session("work")?, &error);
191
192 assert_that!(
193 message,
194 starts_with(format!("{} to delete session work:", "Failed".red().bold()))
195 );
196 assert_that!(message, contains_substring("delete failed"));
197 Ok(())
198 }
199
200 fn listed_session(raw: &str) -> rootcause::Result<SessionName> {
201 raw.parse()
202 }
203}