1use std::path::PathBuf;
2
3use muxr_core::EXTERNAL_LAYOUT_ARG;
4use muxr_core::SessionName;
5use rootcause::report;
6
7pub mod sessions;
8pub mod start;
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum Help {
12 Root,
13 Start,
14}
15
16impl Help {
17 pub(crate) fn from_args(args: &[String]) -> Self {
18 match args.first().map(String::as_str) {
19 Some("start") => Self::Start,
20 _ => Self::Root,
21 }
22 }
23
24 pub const fn text(self) -> &'static str {
25 match self {
26 Self::Root => include_str!("../help.txt"),
27 Self::Start => include_str!("../help/start/help.txt"),
28 }
29 }
30}
31
32#[derive(Clone, Debug, Eq, PartialEq)]
33pub enum Cmd {
34 Help(Help),
35 Sessions,
36 Start {
37 session: SessionName,
38 external_layout: Option<PathBuf>,
39 },
40}
41
42impl Cmd {
43 pub fn from_env() -> rootcause::Result<Self> {
44 let args = ytil_sys::cli::get();
45 let help = Help::from_args(&args);
46 Self::parse(&args).inspect_err(|_| eprintln!("{}", help.text()))
47 }
48
49 fn parse(args: &[String]) -> rootcause::Result<Self> {
50 if args.iter().any(|arg| arg == "--help") {
51 return Ok(Self::Help(Help::from_args(args)));
52 }
53
54 match args {
55 [] => Ok(Self::Sessions),
56 [command, rest @ ..] if command == "start" => Self::parse_start(rest),
57 [command, ..] => Err(report!("unknown muxr cmd {command:?}")),
58 }
59 }
60
61 fn parse_start(args: &[String]) -> rootcause::Result<Self> {
62 match args {
63 [] => Ok(Self::Start {
64 session: SessionName::default(),
65 external_layout: None,
66 }),
67 [layout_flag, layout] if layout_flag == EXTERNAL_LAYOUT_ARG => Ok(Self::Start {
68 session: SessionName::default(),
69 external_layout: Some(PathBuf::from(layout)),
70 }),
71 [layout_flag] if layout_flag == EXTERNAL_LAYOUT_ARG => {
72 Err(report!("missing muxr start layout").attach(format!("flag={EXTERNAL_LAYOUT_ARG}")))
73 }
74 [session] => Ok(Self::Start {
75 session: session.parse()?,
76 external_layout: None,
77 }),
78 [session, layout_flag, layout] if layout_flag == EXTERNAL_LAYOUT_ARG => Ok(Self::Start {
79 session: session.parse()?,
80 external_layout: Some(PathBuf::from(layout)),
81 }),
82 [session, layout_flag] if layout_flag == EXTERNAL_LAYOUT_ARG => {
83 Err(report!("missing muxr start layout").attach(format!("session={session:?}")))
84 }
85 _ => Err(report!("unexpected muxr start args").attach(format!("args={args:?}"))),
86 }
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use std::path::Path;
93
94 use rstest::rstest;
95 use test_that::prelude::*;
96
97 use super::*;
98
99 #[rstest]
100 #[case::start_without_session(&["start"], "default", None)]
101 #[case::start_with_session(&["start", "work"], "work", None)]
102 #[case::start_default_with_layout(
103 &["start", "--layout", "../.config/muxr/layouts/work.json"],
104 "default",
105 Some("../.config/muxr/layouts/work.json")
106 )]
107 #[case::start_session_with_layout(
108 &["start", "work", "--layout", ".config/muxr/layouts/work.json"],
109 "work",
110 Some(".config/muxr/layouts/work.json")
111 )]
112 fn test_parse_when_start_args_vary_returns_start_cmd(
113 #[case] raw: &[&str],
114 #[case] expected_session: &str,
115 #[case] expected_layout: Option<&str>,
116 ) -> rootcause::Result<()> {
117 let parsed = Cmd::parse(&args(raw))?;
118 let Cmd::Start {
119 session,
120 external_layout,
121 } = parsed
122 else {
123 assert_that!(
124 parsed,
125 matches_pattern!(Cmd::Start {
126 session: anything(),
127 external_layout: anything()
128 })
129 );
130 return Ok(());
131 };
132 assert_that!(session.as_ref(), eq(expected_session));
133 assert_that!(external_layout.as_deref().and_then(Path::to_str), eq(expected_layout));
134 Ok(())
135 }
136
137 #[rstest]
138 #[case::help_arg(&["--help"], Help::Root)]
139 #[case::help_among_args(&["start", "--help"], Help::Start)]
140 fn test_parse_when_help_requested_returns_help(
141 #[case] raw: &[&str],
142 #[case] expected: Help,
143 ) -> rootcause::Result<()> {
144 assert_that!(Cmd::parse(&args(raw))?, eq(Cmd::Help(expected)));
145 Ok(())
146 }
147
148 #[test]
149 fn test_parse_when_no_args_returns_session_picker() -> rootcause::Result<()> {
150 assert_that!(Cmd::parse(&args(&[]))?, eq(Cmd::Sessions));
151 Ok(())
152 }
153
154 #[rstest]
155 #[case::start_extra_args(&["start", "work", "extra"])]
156 #[case::start_missing_layout(&["start", "--layout"])]
157 #[case::start_session_missing_layout(&["start", "work", "--layout"])]
158 #[case::start_layout_extra_args(&["start", "work", "--layout", "work", "extra"])]
159 #[case::old_memory_cmd(&["memory"])]
160 #[case::unknown_start_flag(&["start", "--bogus"])]
161 #[case::old_attach_cmd(&["attach"])]
162 #[case::old_detach_cmd(&["detach"])]
163 #[case::old_server_cmd(&["server", "work"])]
164 #[case::unknown_cmd(&["bogus"])]
165 fn test_parse_when_args_are_invalid_returns_error(#[case] raw: &[&str]) {
166 assert_that!(Cmd::parse(&args(raw)), err(anything()));
167 }
168
169 fn args(raw: &[&str]) -> Vec<String> {
170 raw.iter().map(ToString::to_string).collect()
171 }
172}