1use std::fmt::Display;
4use std::fmt::Write;
5use std::process::ExitStatus;
6use std::str::FromStr;
7
8use clap::builder::ValueParserFactory;
9use clap::Arg;
10use clap::ArgAction;
11use clap::Args;
12use clap::FromArgMatches;
13use enum_iterator::Sequence;
14use indoc::indoc;
15use tokio::task::JoinHandle;
16
17use crate::ghci::GhciCommand;
18use crate::maybe_async_command::MaybeAsyncCommand;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Sequence)]
22pub enum LifecycleEvent {
23 Test,
25 Startup(When),
27 Reload(When),
29 Restart(When),
31}
32
33impl Display for LifecycleEvent {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 if let Some(when) = self.when() {
36 write!(f, "{}-", when)?;
37 }
38 write!(f, "{}", self.event_name())
39 }
40}
41
42impl LifecycleEvent {
43 pub fn event_name(&self) -> &'static str {
45 match self {
46 LifecycleEvent::Test => "test",
47 LifecycleEvent::Startup(_) => "startup",
48 LifecycleEvent::Reload(_) => "reload",
49 LifecycleEvent::Restart(_) => "restart",
50 }
51 }
52
53 pub fn event_noun(&self) -> &'static str {
55 match self {
56 LifecycleEvent::Test => "testing",
57 LifecycleEvent::Startup(_) => "starting up",
58 LifecycleEvent::Reload(_) => "reloading",
59 LifecycleEvent::Restart(_) => "restarting",
60 }
61 }
62
63 fn get_message(&self) -> &'static str {
64 match self {
65 LifecycleEvent::Test => indoc!(
66 "
67 Tests are run after startup and after reloads.
68 "
69 ),
70 LifecycleEvent::Startup(_) => indoc!(
71 "
72 Startup hooks run when GHCi is started (at `ghciwatch` startup and after GHCi restarts).
73 "
74 ),
75 LifecycleEvent::Reload(_) => indoc!(
76 "
77 Reload hooks are run when modules are changed on disk.
78 "
79 ),
80 LifecycleEvent::Restart(_) => indoc!(
81 "
82 The GHCi session must be restarted when `.cabal` or `.ghci` files are modified.
83 "
84 ),
85 }.trim_end_matches('\n')
86 }
87
88 fn get_help_name(&self) -> Option<&'static str> {
89 match self {
90 LifecycleEvent::Test => Some("tests"),
91 _ => None,
92 }
93 }
94
95 fn when(&self) -> Option<When> {
96 match &self {
97 LifecycleEvent::Test => None,
98 LifecycleEvent::Startup(when) => Some(*when),
99 LifecycleEvent::Reload(when) => Some(*when),
100 LifecycleEvent::Restart(when) => Some(*when),
101 }
102 }
103
104 fn supported_kind(&self) -> Vec<CommandKind> {
105 match self {
106 LifecycleEvent::Startup(When::Before) => vec![CommandKind::Shell],
107 LifecycleEvent::Startup(When::After)
108 | LifecycleEvent::Test
109 | LifecycleEvent::Reload(_)
110 | LifecycleEvent::Restart(_) => {
111 vec![CommandKind::Ghci, CommandKind::Shell]
112 }
113 }
114 }
115
116 fn hooks() -> impl Iterator<Item = Hook<CommandKind>> {
117 enum_iterator::all::<Self>().flat_map(|event| {
118 event.supported_kind().into_iter().map(move |kind| Hook {
119 event,
120 command: kind,
121 })
122 })
123 }
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Sequence)]
128pub enum When {
129 Before,
131 After,
133}
134
135impl Display for When {
136 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137 match self {
138 When::Before => write!(f, "before"),
139 When::After => write!(f, "after"),
140 }
141 }
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Sequence)]
146pub enum CommandKind {
147 Shell,
149 Ghci,
153}
154
155impl Display for CommandKind {
156 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157 match self {
158 CommandKind::Shell => write!(f, "shell"),
159 CommandKind::Ghci => write!(f, "ghci"),
160 }
161 }
162}
163
164impl CommandKind {
165 fn placeholder_name(&self) -> &'static str {
166 match self {
167 CommandKind::Ghci => "GHCI_CMD",
168 CommandKind::Shell => "SHELL_CMD",
169 }
170 }
171}
172
173#[derive(Debug, Clone)]
175pub enum Command {
176 Shell(MaybeAsyncCommand),
178 Ghci(GhciCommand),
180}
181
182impl Command {
183 fn kind(&self) -> CommandKind {
184 match self {
185 Command::Ghci(_) => CommandKind::Ghci,
186 Command::Shell(_) => CommandKind::Shell,
187 }
188 }
189}
190
191impl Display for Command {
192 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193 match self {
194 Command::Ghci(command) => command.fmt(f),
195 Command::Shell(command) => command.fmt(f),
196 }
197 }
198}
199
200#[derive(Debug, Clone)]
202pub struct Hook<C> {
203 pub event: LifecycleEvent,
205 pub command: C,
207}
208
209impl<C> Display for Hook<C> {
210 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211 self.event.fmt(f)
212 }
213}
214
215impl<C> Hook<C> {
216 fn with_command<C2>(&self, command: C2) -> Hook<C2> {
217 Hook {
218 event: self.event,
219 command,
220 }
221 }
222}
223
224impl Hook<CommandKind> {
225 fn extra_help(&self) -> Option<&'static str> {
226 match (self.event, self.command) {
227 (LifecycleEvent::Startup(When::Before), _) => Some(indoc!(
228 "
229 This can be used to regenerate `.cabal` files with `hpack`.
230 ",
231 )),
232 (LifecycleEvent::Startup(When::After), CommandKind::Ghci) => Some(indoc!(
233 "
234 Use `:set args ...` to set command-line arguments for test hooks.
235 ",
236 )),
237 (LifecycleEvent::Test, CommandKind::Ghci) => Some(indoc!(
238 "
239 Example: `TestMain.testMain`.
240 ",
241 )),
242 _ => None,
243 }
244 .map(|help| help.trim_end_matches('\n'))
245 }
246
247 fn arg_name(&self) -> String {
248 format!("{}-{}", self.event, self.command)
249 }
250
251 fn help(&self) -> Help {
252 let Hook { event, command } = self;
253 let kind = match command {
254 CommandKind::Ghci => "`ghci`",
255 CommandKind::Shell => "Shell",
256 };
257
258 let mut short = format!("{kind} commands to run");
259
260 if let Some(when) = self.event.when() {
261 short.push(' ');
262 write!(short, "{}", when).expect("Writing to a `String` never fails");
263 }
264
265 short.push(' ');
266 if let Some(help_name) = event.get_help_name() {
267 short.push_str(help_name);
268 } else {
269 write!(short, "{}", event.event_name()).expect("Writing to a `String` never fails");
270 }
271
272 let mut long = short.clone();
273
274 long.push_str("\n\n");
275 long.push_str(event.get_message());
276
277 if let CommandKind::Shell = command {
278 long.push_str("\n\nCommands starting with `async:` will be run in the background.");
279 }
280
281 if let Some(extra_help) = self.extra_help() {
282 long.push_str("\n\n");
283 long.push_str(extra_help);
284 }
285
286 long.push_str("\n\nCan be given multiple times.");
287
288 Help { short, long }
289 }
290}
291
292struct Help {
293 short: String,
294 long: String,
295}
296
297#[derive(Debug, Clone, Default)]
302pub struct HookOpts {
303 hooks: Vec<Hook<Command>>,
304}
305
306impl HookOpts {
307 pub fn select(&self, event: LifecycleEvent) -> impl Iterator<Item = &Hook<Command>> {
308 self.hooks.iter().filter(move |hook| hook.event == event)
309 }
310
311 pub async fn run_shell_hooks(
312 &self,
313 event: LifecycleEvent,
314 handles: &mut Vec<JoinHandle<miette::Result<ExitStatus>>>,
315 ) -> miette::Result<()> {
316 for hook in self.select(event) {
317 if let Command::Shell(command) = &hook.command {
318 tracing::info!(%command, "Running {hook} command");
319 command.run_on(handles).await?;
320 }
321 }
322 Ok(())
323 }
324}
325
326impl Args for HookOpts {
327 fn augment_args(mut cmd: clap::Command) -> clap::Command {
328 for hook in LifecycleEvent::hooks() {
329 let name = hook.arg_name();
330 let help = hook.help();
331 let arg = Arg::new(&name)
332 .long(&name)
333 .action(ArgAction::Append)
334 .required(false)
335 .value_name(hook.command.placeholder_name())
336 .help(help.short)
337 .long_help(help.long)
338 .help_heading("Lifecycle hooks");
339
340 let arg = match hook.command {
341 CommandKind::Ghci => arg.value_parser(GhciCommand::value_parser()),
342 CommandKind::Shell => arg.value_parser(MaybeAsyncCommand::from_str),
343 };
344
345 cmd = cmd.arg(arg);
346 }
347 cmd
348 }
349
350 fn augment_args_for_update(cmd: clap::Command) -> clap::Command {
351 Self::augment_args(cmd)
352 }
353}
354
355impl FromArgMatches for HookOpts {
356 fn from_arg_matches(matches: &clap::ArgMatches) -> Result<Self, clap::Error> {
357 let mut ret = Self::default();
358 ret.update_from_arg_matches(matches)?;
359 Ok(ret)
360 }
361
362 fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> {
363 for hook in LifecycleEvent::hooks() {
364 let name = hook.arg_name();
365 match hook.command {
366 CommandKind::Ghci => {
367 self.hooks.extend(
368 matches
369 .get_many::<GhciCommand>(&name)
370 .into_iter()
371 .flatten()
372 .map(|command| hook.with_command(Command::Ghci(command.clone()))),
373 );
374 }
375 CommandKind::Shell => {
376 self.hooks.extend(
377 matches
378 .get_many::<MaybeAsyncCommand>(&name)
379 .into_iter()
380 .flatten()
381 .map(|command| hook.with_command(Command::Shell(command.clone()))),
382 );
383 }
384 }
385 }
386
387 self.hooks
392 .sort_by(|a, b| a.command.kind().cmp(&b.command.kind()));
393
394 Ok(())
395 }
396}