1use std::{
8 collections::BTreeMap,
9 io::{self, IsTerminal, Write},
10 ops::ControlFlow,
11};
12
13use color_eyre::{eyre::eyre, Result};
14use colored::Colorize;
15use gammalooprs::integrands::process::ProcessIntegrand;
16use gammalooprs::model::Model;
17use gammalooprs::processes::{
18 GraphSelectionSignatureInventory, ProcessCollection, ProcessDefinition,
19 RaisedCutSignatureInventory,
20};
21use gammalooprs::settings::{global::GenerationSettings, RuntimeSettings};
22use tracing::{info, warn};
23
24use crate::{
25 command_template::contains_placeholder,
26 commands::{
27 run::{
28 prepare_command_histories_with_context, PreparedCommand, PreparedRun, MAX_RUN_DEPTH,
29 },
30 save::SaveState,
31 CommandExecution, Commands, StartCommandsBlock,
32 },
33 completion::{
34 IntegrandDetailCompletionEntry, IrProfileCompletionEntry, ModelVertexCompletionEntry,
35 ProcessCompletionEntry, ProcessKind,
36 },
37 integrand_info::IntegrandKind,
38 render_smart_toml,
39 state::{CommandHistory, CommandsBlock, ProcessRef, RunHistory, State},
40 CLISettings, ReadOnlyStateOrigin,
41};
42
43#[cfg(feature = "cli")]
44use crate::commands::process_settings::{
45 serialize_runtime_named_settings, ProcessSettingsCompletionEntry,
46};
47
48const MAX_DISPLAY_COMMAND_LINES: usize = 5;
49
50fn format_command_block_conflict_message(conflicting_blocks: &[String]) -> String {
51 match conflicting_blocks {
52 [] => String::new(),
53 [single] => format!(
54 "Run card command block '{}' redefines an existing block with different commands",
55 single
56 ),
57 many => format!(
58 "Run card command blocks {} redefine existing blocks with different commands",
59 many.iter()
60 .map(|name| format!("'{}'", name))
61 .collect::<Vec<_>>()
62 .join(", ")
63 ),
64 }
65}
66
67fn prompt_command_block_conflict_override(conflicting_blocks: &[String]) -> Result<bool> {
68 let prompt = format!(
69 "{}. Proceed anyway? (it may break reproducibility of the state from run.toml) [y/n] > ",
70 format_command_block_conflict_message(conflicting_blocks)
71 );
72 let mut stderr = io::stderr();
73 let mut input = String::new();
74 loop {
75 eprint!("{prompt}");
76 stderr.flush()?;
77 input.clear();
78 io::stdin().read_line(&mut input)?;
79 match input.trim().to_ascii_lowercase().as_str() {
80 "y" | "yes" => return Ok(true),
81 "n" | "no" => return Ok(false),
82 _ => {
83 eprintln!("Please answer 'y' or 'n'.");
84 }
85 }
86 }
87}
88
89#[derive(Debug, Clone, Default)]
94pub struct CliSessionState {
95 pending_commands_block: Option<PendingCommandsBlock>,
96}
97
98#[derive(Debug, Clone)]
99struct PendingCommandsBlock {
100 name: String,
101 commands: Vec<CommandHistory>,
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105enum HistoryMode {
106 Record,
107 Suppress,
108}
109
110pub struct CliSession<'a> {
117 state: &'a mut State,
118 run_history: &'a mut RunHistory,
119 cli_settings: &'a mut CLISettings,
120 default_runtime_settings: &'a mut RuntimeSettings,
121 session_state: &'a mut CliSessionState,
122}
123
124impl<'a> CliSession<'a> {
125 pub fn new(
131 state: &'a mut State,
132 run_history: &'a mut RunHistory,
133 cli_settings: &'a mut CLISettings,
134 default_runtime_settings: &'a mut RuntimeSettings,
135 session_state: &'a mut CliSessionState,
136 ) -> Self {
137 Self {
138 state,
139 run_history,
140 cli_settings,
141 default_runtime_settings,
142 session_state,
143 }
144 }
145
146 pub fn execute_command(&mut self, command: CommandHistory) -> Result<CommandExecution> {
159 let prepared = PreparedCommand::prepare(command, self.run_history, 1)?;
160 self.execute_prepared(prepared, HistoryMode::Record)
161 }
162
163 pub fn replay_run_history(&mut self) -> Result<ControlFlow<SaveState>> {
168 self.run_history
169 .apply_session_settings(self.cli_settings, self.default_runtime_settings)?;
170 let prepared = prepare_command_histories_with_context(
171 &self.run_history.commands.clone(),
172 self.run_history,
173 1,
174 "run history",
175 )?;
176 self.execute_prepared_commands(prepared, HistoryMode::Suppress)
177 }
178
179 pub fn apply_boot_run_history(
180 &mut self,
181 boot_run_history: &RunHistory,
182 effective_boot_run_history: &RunHistory,
183 booted_existing_state: bool,
184 ) -> Result<ControlFlow<SaveState>> {
185 let interactive_prompt_available = io::stdin().is_terminal() && io::stdout().is_terminal();
186 self.apply_boot_run_history_with_conflict_resolver(
187 boot_run_history,
188 effective_boot_run_history,
189 booted_existing_state,
190 |conflicting_blocks| {
191 if interactive_prompt_available {
192 return prompt_command_block_conflict_override(conflicting_blocks);
193 }
194
195 Err(eyre!(
196 "{}. Cannot continue non-interactively; rerun in an interactive terminal or align the command block definitions.",
197 format_command_block_conflict_message(conflicting_blocks)
198 ))
199 },
200 )
201 }
202
203 pub fn apply_boot_run_history_with_conflict_resolver<F>(
204 &mut self,
205 boot_run_history: &RunHistory,
206 effective_boot_run_history: &RunHistory,
207 booted_existing_state: bool,
208 mut resolve_conflicts: F,
209 ) -> Result<ControlFlow<SaveState>>
210 where
211 F: FnMut(&[String]) -> Result<bool>,
212 {
213 boot_run_history.validate()?;
214
215 if booted_existing_state
216 && !self
217 .run_history
218 .frozen_boot_settings_match(boot_run_history)
219 {
220 let warning = format!(
221 "Boot card settings differ from the frozen settings stored in {}. This session is forced into --read-only-state. Line up cli_settings.global and default_runtime_settings in the boot card with the frozen settings in run.toml if you want to boot this state without read-only mode.",
222 self.cli_settings.state.folder.join("run.toml").display()
223 );
224 self.cli_settings
225 .session
226 .force_read_only_state(ReadOnlyStateOrigin::BootSettingsMismatch);
227 self.cli_settings
228 .session
229 .startup_warnings
230 .push(warning.clone());
231 warn!("{warning}");
232 }
233 if !booted_existing_state {
234 self.run_history.freeze_boot_settings_from(boot_run_history);
235 }
236
237 let mut merged_history = self.run_history.clone();
238 let conflicting_blocks = merged_history
239 .conflicting_command_block_names(&effective_boot_run_history.command_blocks);
240 let overwrite_conflicting_blocks = if conflicting_blocks.is_empty() {
241 false
242 } else if self.cli_settings.session.read_only_state {
243 if self
244 .cli_settings
245 .session
246 .is_read_only_due_to_boot_settings_mismatch()
247 {
248 let warning = format!(
249 "{}. The boot card settings differ from the frozen settings stored in {}; this session remains in --read-only-state for reproducibility, but will use the boot card command block definitions for this process only.",
250 format_command_block_conflict_message(&conflicting_blocks),
251 self.cli_settings.state.folder.join("run.toml").display()
252 );
253 self.cli_settings
254 .session
255 .startup_warnings
256 .push(warning.clone());
257 warn!("{warning}");
258 true
259 } else {
260 return Err(eyre!(
261 "{}. Cannot proceed while --read-only-state is enabled.",
262 format_command_block_conflict_message(&conflicting_blocks)
263 ));
264 }
265 } else if resolve_conflicts(&conflicting_blocks)? {
266 true
267 } else {
268 info!(
269 "{}",
270 "Boot run card application cancelled by user.".yellow()
271 );
272 return Ok(ControlFlow::Break(SaveState::default()));
273 };
274 merged_history.merge_command_blocks_with_overwrite(
275 &effective_boot_run_history.command_blocks,
276 overwrite_conflicting_blocks,
277 )?;
278
279 for block in &effective_boot_run_history.command_blocks {
280 let mut pending = vec![(
284 format!("command block '{}'", block.name),
285 block.commands.clone(),
286 vec![block.name.clone()],
287 2,
288 )];
289 while let Some((context, commands, active_blocks, depth)) = pending.pop() {
290 for (index, command) in commands.into_iter().enumerate() {
291 let Commands::Run(run) = command.command else {
292 continue;
293 };
294 if depth > MAX_RUN_DEPTH {
295 return Err(eyre!(
296 "Maximum nested run depth of {} reached while validating {}",
297 MAX_RUN_DEPTH,
298 context
299 ));
300 }
301 let names = run
302 .selected_block_names()
303 .iter()
304 .filter(|name| !contains_placeholder(name))
305 .cloned()
306 .collect::<Vec<_>>();
307 let selected = merged_history
308 .select_command_blocks(&names)
309 .map_err(|err| {
310 eyre!(
311 "Failed to validate {} command #{}: {}",
312 context,
313 index + 1,
314 err
315 )
316 })?;
317 for nested in selected.into_iter().rev() {
318 if let Some(start) =
319 active_blocks.iter().position(|name| *name == nested.name)
320 {
321 let mut cycle = active_blocks[start..].to_vec();
322 cycle.push(nested.name.clone());
323 return Err(eyre!(
324 "Command block recursion detected: {}",
325 cycle.join(" -> ")
326 ));
327 }
328 let mut active = active_blocks.clone();
329 active.push(nested.name.clone());
330 pending.push((
331 format!("command block '{}'", nested.name),
332 nested.commands,
333 active,
334 depth + 1,
335 ));
336 }
337 let inline = run.parse_inline_commands()?;
338 if !inline.is_empty() {
339 pending.push((
340 format!("{} run -c", context),
341 inline,
342 active_blocks.clone(),
343 depth + 1,
344 ));
345 }
346 }
347 }
348 }
349
350 let prepared = prepare_command_histories_with_context(
351 &effective_boot_run_history.commands,
352 &merged_history,
353 1,
354 "boot",
355 )?;
356
357 self.run_history.merge_command_blocks_with_overwrite(
358 &effective_boot_run_history.command_blocks,
359 overwrite_conflicting_blocks,
360 )?;
361 effective_boot_run_history
362 .apply_session_settings(self.cli_settings, self.default_runtime_settings)?;
363 self.execute_prepared_commands(prepared, HistoryMode::Record)
364 }
365
366 pub fn has_pending_commands_block(&self) -> bool {
367 self.session_state.pending_commands_block.is_some()
368 }
369
370 pub fn pending_commands_block_name(&self) -> Option<String> {
371 self.session_state
372 .pending_commands_block
373 .as_ref()
374 .map(|pending| pending.name.clone())
375 }
376
377 pub fn prompt_state_label(&self) -> String {
378 self.cli_settings.state.prompt_label()
379 }
380
381 pub fn current_commands_block_names(&self) -> Vec<String> {
382 self.run_history
383 .command_blocks
384 .iter()
385 .map(|block| block.name.clone())
386 .collect()
387 }
388
389 pub fn current_command_block_placeholders(
390 &self,
391 ) -> BTreeMap<String, Vec<crate::command_template::PlaceholderSpec>> {
392 self.run_history
393 .command_blocks
394 .iter()
395 .map(|block| {
396 (
397 block.name.clone(),
398 self.run_history
399 .command_block_placeholder_specs(&block.name)
400 .into_iter()
401 .collect(),
402 )
403 })
404 .collect()
405 }
406
407 pub fn current_process_entries(&self) -> Vec<ProcessCompletionEntry> {
408 self.state
409 .process_list
410 .processes
411 .iter()
412 .enumerate()
413 .map(|(id, process)| ProcessCompletionEntry {
414 id,
415 name: process.definition.folder_name.clone(),
416 kind: match &process.collection {
417 ProcessCollection::Amplitudes(_) => ProcessKind::Amplitude,
418 ProcessCollection::CrossSections(_) => ProcessKind::CrossSection,
419 },
420 integrand_names: process
421 .collection
422 .get_integrand_names()
423 .into_iter()
424 .map(str::to_string)
425 .collect(),
426 })
427 .collect()
428 }
429
430 pub fn current_integrand_detail_entries(&self) -> Vec<IntegrandDetailCompletionEntry> {
431 self.state
432 .process_list
433 .processes
434 .iter()
435 .enumerate()
436 .flat_map(|(process_id, process)| {
437 process
438 .collection
439 .get_integrand_names()
440 .into_iter()
441 .filter_map(move |integrand_name| {
442 let integrand_name = integrand_name.to_string();
443 let info = self
444 .state
445 .get_integrand_info(
446 Some(&ProcessRef::Id(process_id)),
447 Some(&integrand_name),
448 )
449 .ok()?;
450 let mut master_graph_names = info
451 .graph_groups
452 .iter()
453 .filter_map(|group| {
454 group
455 .graphs
456 .iter()
457 .find(|graph| graph.is_master)
458 .map(|graph| graph.name.clone())
459 })
460 .collect::<Vec<_>>();
461 master_graph_names.sort();
462 master_graph_names.dedup();
463
464 let mut categories = vec![
465 "generation".to_string(),
466 "orientation".to_string(),
467 "loop_momentum_basis".to_string(),
468 ];
469 if info.kind == IntegrandKind::CrossSection {
470 categories.push("cuts".to_string());
471 }
472 let signature_inventory =
473 integrand_signature_inventory(&process.collection, &integrand_name);
474 let amplitude_signature_inventory = amplitude_graph_signature_inventory(
475 &process.collection,
476 &integrand_name,
477 &self.state.model,
478 &process.definition,
479 &self.cli_settings.global.generation,
480 );
481 let raised_cut_signature_inventory = raised_cut_signature_inventory(
482 &process.collection,
483 &integrand_name,
484 &self.state.model,
485 &process.definition,
486 &self.cli_settings.global.generation,
487 );
488
489 Some(IntegrandDetailCompletionEntry {
490 process_name: process.definition.folder_name.clone(),
491 integrand_name,
492 kind: info.kind,
493 master_graph_names,
494 raised_all_signatures: signature_inventory.raised_all,
495 raised_massive_signatures: signature_inventory.raised_massive,
496 raised_massless_signatures: signature_inventory.raised_massless,
497 cycle_signatures: signature_inventory.cycles,
498 amplitude_raised_all_signatures: amplitude_signature_inventory
499 .raised_all,
500 amplitude_raised_massive_signatures: amplitude_signature_inventory
501 .raised_massive,
502 amplitude_raised_massless_signatures: amplitude_signature_inventory
503 .raised_massless,
504 amplitude_cycle_signatures: amplitude_signature_inventory.cycles,
505 raised_cut_all_signatures: raised_cut_signature_inventory.all,
506 raised_cut_massive_signatures: raised_cut_signature_inventory.massive,
507 raised_cut_massless_signatures: raised_cut_signature_inventory.massless,
508 categories,
509 })
510 })
511 .collect::<Vec<_>>()
512 })
513 .collect()
514 }
515
516 pub fn current_ir_profile_entries(&self) -> Vec<IrProfileCompletionEntry> {
517 self.state
518 .process_list
519 .processes
520 .iter()
521 .filter_map(|process| {
522 let ProcessCollection::CrossSections(cross_sections) = &process.collection else {
523 return None;
524 };
525
526 Some(
527 cross_sections
528 .iter()
529 .filter_map(|(integrand_name, cross_section)| {
530 let ProcessIntegrand::CrossSection(integrand) =
531 cross_section.integrand.as_ref()?
532 else {
533 return None;
534 };
535 let completion_entries = integrand.ir_profile_completion_entries();
536 let mut graph_names = completion_entries
537 .iter()
538 .map(|(graph_name, _)| graph_name.clone())
539 .collect::<Vec<_>>();
540 graph_names.sort();
541 graph_names.dedup();
542
543 let mut graph_limit_entries = completion_entries
544 .into_iter()
545 .flat_map(|(graph_name, limits)| {
546 limits
547 .into_iter()
548 .map(move |limit| format!("{graph_name} {limit}"))
549 })
550 .collect::<Vec<_>>();
551 graph_limit_entries.sort();
552 graph_limit_entries.dedup();
553
554 Some(IrProfileCompletionEntry {
555 process_name: process.definition.folder_name.clone(),
556 integrand_name: integrand_name.clone(),
557 graph_names,
558 graph_limit_entries,
559 })
560 })
561 .collect::<Vec<_>>(),
562 )
563 })
564 .flatten()
565 .collect()
566 }
567
568 #[cfg(feature = "cli")]
569 pub(crate) fn current_process_settings_entries(&self) -> Vec<ProcessSettingsCompletionEntry> {
570 let mut entries = Vec::new();
571
572 for (process_id, process) in self.state.process_list.processes.iter().enumerate() {
573 for integrand_name in process.collection.get_integrand_names() {
574 let Ok(resolved) = process.get_integrand(integrand_name) else {
575 continue;
576 };
577 let Some(integrand) = resolved.integrand else {
578 continue;
579 };
580 let Ok(serialized) = serialize_runtime_named_settings(integrand.get_settings())
581 else {
582 continue;
583 };
584
585 entries.push(ProcessSettingsCompletionEntry {
586 process_id,
587 process_name: process.definition.folder_name.clone(),
588 integrand_name: integrand_name.to_string(),
589 quantities: serialized.quantities,
590 observables: serialized.observables,
591 selectors: serialized.selectors,
592 });
593 }
594 }
595
596 entries.sort_by(|left, right| {
597 left.process_id
598 .cmp(&right.process_id)
599 .then_with(|| left.integrand_name.cmp(&right.integrand_name))
600 });
601 entries
602 }
603
604 pub fn current_model_parameter_entries(
605 &self,
606 ) -> Vec<crate::completion::ModelParameterCompletionEntry> {
607 let mut entries = self
608 .state
609 .model_parameters
610 .keys()
611 .filter_map(|name| {
612 self.state
613 .model
614 .get_parameter_opt(name.to_string())
615 .map(
616 |parameter| crate::completion::ModelParameterCompletionEntry {
617 name: name.to_string(),
618 parameter_type: parameter.parameter_type.clone(),
619 },
620 )
621 })
622 .collect::<Vec<_>>();
623 entries.sort_by(|left, right| left.name.cmp(&right.name));
624 entries
625 }
626
627 pub fn current_model_particle_names(&self) -> Vec<String> {
628 let mut names = self
629 .state
630 .model
631 .particles
632 .iter()
633 .flat_map(|particle| [particle.name.to_string(), particle.antiname.to_string()])
634 .collect::<Vec<_>>();
635 names.sort();
636 names.dedup();
637 names
638 }
639
640 pub fn current_model_select_particle_names(&self) -> Vec<String> {
641 let mut names = self
642 .state
643 .model
644 .particles
645 .iter()
646 .filter(|particle| particle.pdg_code > 0)
647 .map(|particle| particle.name.to_string())
648 .collect::<Vec<_>>();
649 names.sort();
650 names.dedup();
651 names
652 }
653
654 pub fn current_model_coupling_names(&self) -> Vec<String> {
655 let mut names = self
656 .state
657 .model
658 .orders
659 .iter()
660 .map(|order| order.name.to_string())
661 .collect::<Vec<_>>();
662 names.sort();
663 names.dedup();
664 names
665 }
666
667 pub fn current_model_vertices(&self) -> Vec<ModelVertexCompletionEntry> {
668 let mut entries = self
669 .state
670 .model
671 .vertex_rules
672 .iter()
673 .map(|vertex_rule| ModelVertexCompletionEntry {
674 name: vertex_rule.0.name.to_string(),
675 particles: vertex_rule
676 .0
677 .particles
678 .iter()
679 .map(|particle| particle.name.to_string())
680 .collect(),
681 })
682 .collect::<Vec<_>>();
683 entries.sort_by(|left, right| left.name.cmp(&right.name));
684 entries.dedup_by(|left, right| left.name == right.name);
685 entries
686 }
687
688 pub fn run_history_toml(&self) -> Result<String> {
689 self.run_history
690 .to_toml_string(self.cli_settings.try_strings)
691 }
692
693 pub fn global_settings_toml(&self) -> Result<String> {
694 render_smart_toml(self.cli_settings)
695 }
696
697 pub fn active_command_blocks(&self) -> BTreeMap<String, Vec<String>> {
698 self.run_history
699 .command_blocks
700 .iter()
701 .map(|block| {
702 (
703 block.name.clone(),
704 block.commands.iter().map(display_command).collect(),
705 )
706 })
707 .collect()
708 }
709
710 pub fn dismiss_pending_commands_block(&mut self, trigger: &str) -> bool {
711 let Some(pending) = self.session_state.pending_commands_block.take() else {
712 return false;
713 };
714
715 info!(
716 "{} {} {} {}",
717 "Dismissing command block definition".blue(),
718 pending.name.green(),
719 "after".blue(),
720 trigger.green()
721 );
722 true
723 }
724
725 fn execute_prepared_commands(
726 &mut self,
727 commands: Vec<PreparedCommand>,
728 history_mode: HistoryMode,
729 ) -> Result<ControlFlow<SaveState>> {
730 for command in commands {
731 let execution = self.execute_prepared(command, history_mode)?;
732 if let ControlFlow::Break(save_state) = execution.flow {
733 return Ok(ControlFlow::Break(save_state));
734 }
735 }
736 Ok(ControlFlow::Continue(()))
737 }
738
739 fn execute_prepared(
740 &mut self,
741 command: PreparedCommand,
742 history_mode: HistoryMode,
743 ) -> Result<CommandExecution> {
744 match command {
745 PreparedCommand::Plain(command) => self.execute_plain(command, history_mode),
746 PreparedCommand::Run { command, plan } => self.execute_run(command, plan, history_mode),
747 }
748 }
749
750 fn execute_plain(
751 &mut self,
752 command: CommandHistory,
753 history_mode: HistoryMode,
754 ) -> Result<CommandExecution> {
755 if self.session_state.pending_commands_block.is_some() {
756 return self.handle_recording_mode(command);
757 }
758
759 let display_text = display_command(&command);
760 info!("{} {}", "Running command".blue(), display_text.green());
761
762 let record = match &command.command {
763 Commands::StartCommandsBlock(start) => {
764 self.start_commands_block(start.clone())?;
765 false
766 }
767 Commands::FinishCommandsBlock => {
768 self.finish_commands_block()?;
769 false
770 }
771 _ => {
772 let execution = command.command.clone().run(
773 self.state,
774 self.run_history,
775 self.cli_settings,
776 self.default_runtime_settings,
777 )?;
778 if let ControlFlow::Break(save_state) = execution.flow {
779 if history_mode == HistoryMode::Record {
780 self.record_command(&command);
781 }
782 return Ok(CommandExecution::break_with(save_state));
783 }
784 let output = execution.output;
785 if history_mode == HistoryMode::Record {
786 self.record_command(&command);
787 }
788 return Ok(CommandExecution::continue_with(output));
789 }
790 };
791
792 if record {
793 self.record_command(&command);
794 }
795
796 Ok(CommandExecution::continue_without_output())
797 }
798
799 fn execute_run(
800 &mut self,
801 command: CommandHistory,
802 plan: PreparedRun,
803 history_mode: HistoryMode,
804 ) -> Result<CommandExecution> {
805 if self.session_state.pending_commands_block.is_some() {
806 return self.handle_recording_mode(command);
807 }
808
809 if plan.is_empty() {
810 return Ok(CommandExecution::continue_without_output());
811 }
812
813 let display_text = display_command(&command);
814 info!("{} {}", "Running command".blue(), display_text.green());
815
816 for block in plan.blocks {
817 info!("{} {}", "Starting command block".blue(), block.name.green());
818 if let ControlFlow::Break(save_state) =
819 self.execute_prepared_commands(block.commands, HistoryMode::Suppress)?
820 {
821 if history_mode == HistoryMode::Record {
822 self.record_command(&command);
823 }
824 return Ok(CommandExecution::break_with(save_state));
825 }
826 }
827
828 if !plan.commands.is_empty() {
829 info!(
830 "{} {}",
831 "Starting commands supplied with".blue(),
832 "--commands/-c".green()
833 );
834 }
835
836 if let ControlFlow::Break(save_state) =
837 self.execute_prepared_commands(plan.commands, HistoryMode::Suppress)?
838 {
839 if history_mode == HistoryMode::Record {
840 self.record_command(&command);
841 }
842 return Ok(CommandExecution::break_with(save_state));
843 }
844
845 if history_mode == HistoryMode::Record {
846 self.record_command(&command);
847 }
848
849 Ok(CommandExecution::continue_without_output())
850 }
851
852 fn handle_recording_mode(&mut self, command: CommandHistory) -> Result<CommandExecution> {
853 match command.command.clone() {
854 Commands::StartCommandsBlock(_) => Err(eyre!(
855 "Cannot start a new command block definition before finishing the current one"
856 )),
857 Commands::FinishCommandsBlock => {
858 self.finish_commands_block()?;
859 Ok(CommandExecution::continue_without_output())
860 }
861 Commands::Quit(_) => {
862 self.dismiss_pending_commands_block("quit");
863 Ok(CommandExecution::continue_without_output())
864 }
865 _ => {
866 let stored = normalize_command_history(&command);
867 let pending = self
868 .session_state
869 .pending_commands_block
870 .as_mut()
871 .expect("recording mode requires a pending block");
872 pending.commands.push(stored);
873 Ok(CommandExecution::continue_without_output())
874 }
875 }
876 }
877
878 fn start_commands_block(&mut self, start: StartCommandsBlock) -> Result<()> {
879 if self.session_state.pending_commands_block.is_some() {
880 return Err(eyre!(
881 "Cannot start a new command block definition before finishing the current one"
882 ));
883 }
884
885 if self.run_history.command_block(&start.name).is_some() {
886 info!(
887 "{} {}",
888 "Overwriting existing command block definition".blue(),
889 start.name.green()
890 );
891 } else {
892 info!(
893 "{} {}",
894 "Defining new command block definition".blue(),
895 start.name.green()
896 );
897 }
898
899 self.session_state.pending_commands_block = Some(PendingCommandsBlock {
900 name: start.name,
901 commands: Vec::new(),
902 });
903 Ok(())
904 }
905
906 fn finish_commands_block(&mut self) -> Result<()> {
907 let Some(pending) = self.session_state.pending_commands_block.take() else {
908 return Err(eyre!(
909 "No command block definition is currently being recorded"
910 ));
911 };
912
913 let block = CommandsBlock {
914 name: pending.name.clone(),
915 commands: pending.commands,
916 };
917
918 if let Some(existing_index) = self
919 .run_history
920 .command_blocks
921 .iter()
922 .position(|existing| existing.name == pending.name)
923 {
924 self.run_history.command_blocks[existing_index] = block;
925 } else {
926 self.run_history.command_blocks.push(block);
927 }
928
929 self.run_history.validate()?;
930 Ok(())
931 }
932
933 fn record_command(&mut self, command: &CommandHistory) {
934 if let Some(normalized) = normalize_persisted_command_history(command) {
935 self.run_history
936 .push_with_raw(normalized.command, normalized.raw_string);
937 }
938 }
939}
940
941fn integrand_signature_inventory(
942 collection: &ProcessCollection,
943 integrand_name: &str,
944) -> GraphSelectionSignatureInventory {
945 match collection {
946 ProcessCollection::Amplitudes(amplitudes) => amplitudes
947 .get(integrand_name)
948 .map(|amplitude| {
949 GraphSelectionSignatureInventory::from_master_graphs(
950 amplitude.graph_group_structure.iter().map(|group| {
951 let master_graph_id = group
952 .into_iter()
953 .next()
954 .expect("graph group should contain a master graph");
955 &litude.graphs[master_graph_id].graph
956 }),
957 )
958 })
959 .unwrap_or_else(GraphSelectionSignatureInventory::empty),
960 ProcessCollection::CrossSections(cross_sections) => cross_sections
961 .get(integrand_name)
962 .map(|cross_section| {
963 GraphSelectionSignatureInventory::from_master_graphs(
964 cross_section.graph_group_structure.iter().map(|group| {
965 let master_graph_id = group
966 .into_iter()
967 .next()
968 .expect("graph group should contain a master graph");
969 &cross_section.supergraphs[master_graph_id].graph
970 }),
971 )
972 })
973 .unwrap_or_else(GraphSelectionSignatureInventory::empty),
974 }
975}
976
977fn amplitude_graph_signature_inventory(
978 collection: &ProcessCollection,
979 integrand_name: &str,
980 model: &Model,
981 process_definition: &ProcessDefinition,
982 generation_settings: &GenerationSettings,
983) -> GraphSelectionSignatureInventory {
984 let ProcessCollection::CrossSections(cross_sections) = collection else {
985 return GraphSelectionSignatureInventory::empty();
986 };
987 cross_sections
988 .get(integrand_name)
989 .map(|cross_section| {
990 cross_section
991 .amplitude_graph_signature_inventory(model, process_definition, generation_settings)
992 .unwrap_or_else(|_| GraphSelectionSignatureInventory::empty())
993 })
994 .unwrap_or_else(GraphSelectionSignatureInventory::empty)
995}
996
997fn raised_cut_signature_inventory(
998 collection: &ProcessCollection,
999 integrand_name: &str,
1000 model: &Model,
1001 process_definition: &ProcessDefinition,
1002 generation_settings: &GenerationSettings,
1003) -> RaisedCutSignatureInventory {
1004 let ProcessCollection::CrossSections(cross_sections) = collection else {
1005 return RaisedCutSignatureInventory::empty();
1006 };
1007 cross_sections
1008 .get(integrand_name)
1009 .map(|cross_section| {
1010 cross_section
1011 .raised_cut_signature_inventory(model, process_definition, generation_settings)
1012 .unwrap_or_else(|_| RaisedCutSignatureInventory::empty())
1013 })
1014 .unwrap_or_else(RaisedCutSignatureInventory::empty)
1015}
1016
1017fn normalize_persisted_command_history(command: &CommandHistory) -> Option<CommandHistory> {
1018 match &command.command {
1019 Commands::Quit(_) | Commands::StartCommandsBlock(_) | Commands::FinishCommandsBlock => None,
1020 Commands::Run(run) => normalize_persisted_run_history(command, run),
1021 _ => Some(normalize_command_history(command)),
1022 }
1023}
1024
1025fn normalize_persisted_run_history(
1026 command: &CommandHistory,
1027 run: &crate::commands::Run,
1028) -> Option<CommandHistory> {
1029 let inline_commands = run
1030 .parse_inline_commands()
1031 .expect("persisted run commands should always have parseable inline commands");
1032 let persisted_inline_commands = inline_commands
1033 .iter()
1034 .filter_map(normalize_persisted_command_history)
1035 .collect::<Vec<_>>();
1036
1037 let persisted_run = crate::commands::Run {
1038 block_names: run.block_names.clone(),
1039 defines: run.defines.clone(),
1040 commands: (!persisted_inline_commands.is_empty()).then(|| {
1041 persisted_inline_commands
1042 .iter()
1043 .map(persisted_command_raw_string)
1044 .collect::<Vec<_>>()
1045 .join("; ")
1046 }),
1047 };
1048
1049 if persisted_run.is_noop() {
1050 return None;
1051 }
1052
1053 if persisted_run == *run {
1054 return Some(normalize_command_history(command));
1055 }
1056
1057 let raw_string = persisted_run.canonical_raw_string();
1058 Some(CommandHistory::new_with_raw(
1059 Commands::Run(persisted_run),
1060 raw_string,
1061 ))
1062}
1063
1064fn persisted_command_raw_string(command: &CommandHistory) -> String {
1065 normalize_command_history(command)
1066 .raw_string()
1067 .expect("persisted inline commands should remain representable as raw strings")
1068 .to_string()
1069}
1070
1071fn normalize_command_history(command: &CommandHistory) -> CommandHistory {
1072 let raw_string = command
1073 .raw_string()
1074 .filter(|raw| raw_round_trips(raw, &command.command))
1075 .map(str::to_string)
1076 .or_else(|| match &command.command {
1077 Commands::Run(run) => Some(run.canonical_raw_string()),
1078 _ => None,
1079 });
1080
1081 CommandHistory {
1082 command: command.command.clone(),
1083 raw_string,
1084 }
1085}
1086
1087fn raw_round_trips(raw: &str, command: &Commands) -> bool {
1088 CommandHistory::from_raw_string(raw)
1089 .map(|parsed| parsed.command == *command)
1090 .unwrap_or(false)
1091}
1092
1093pub(crate) fn display_command(command: &CommandHistory) -> String {
1094 let display = if let Some(raw_string) = normalize_command_history(command).raw_string {
1095 raw_string
1096 } else {
1097 format!("{:?}", command.command)
1098 };
1099
1100 truncate_multiline_display_command(&display)
1101}
1102
1103fn truncate_multiline_display_command(display: &str) -> String {
1104 let lines = display.lines().collect::<Vec<_>>();
1105 if lines.len() <= MAX_DISPLAY_COMMAND_LINES {
1106 return display.to_string();
1107 }
1108
1109 let mut truncated = lines[..MAX_DISPLAY_COMMAND_LINES].join("\n");
1110 truncated.push_str("\n[...]");
1111 truncated
1112}
1113
1114#[cfg(test)]
1115mod tests {
1116 use super::{display_command, MAX_DISPLAY_COMMAND_LINES};
1117 use crate::{commands::run::Run, commands::Commands, state::CommandHistory};
1118
1119 #[test]
1120 fn display_command_preserves_multiline_commands_up_to_line_limit() {
1121 let commands = (1..=MAX_DISPLAY_COMMAND_LINES)
1122 .map(|i| format!("cmd{i}"))
1123 .collect::<Vec<_>>()
1124 .join("\n");
1125 let command = CommandHistory::new(Commands::Run(Run {
1126 block_names: Vec::new(),
1127 defines: Vec::new(),
1128 commands: Some(commands.clone()),
1129 }));
1130
1131 let displayed = display_command(&command);
1132
1133 assert_eq!(displayed.lines().count(), MAX_DISPLAY_COMMAND_LINES);
1134 assert!(displayed.contains(&commands));
1135 assert!(!displayed.contains("[...]"));
1136 }
1137
1138 #[test]
1139 fn display_command_truncates_multiline_commands_beyond_line_limit() {
1140 let command = CommandHistory::new(Commands::Run(Run {
1141 block_names: Vec::new(),
1142 defines: Vec::new(),
1143 commands: Some("cmd1\ncmd2\ncmd3\ncmd4\ncmd5\ncmd6".to_string()),
1144 }));
1145
1146 let displayed = display_command(&command);
1147 let displayed_lines = displayed.lines().collect::<Vec<_>>();
1148
1149 assert_eq!(
1150 displayed_lines,
1151 vec!["run -c 'cmd1", "cmd2", "cmd3", "cmd4", "cmd5", "[...]"]
1152 );
1153 }
1154}