Skip to main content

muxr/
cmds.rs

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}