1use regex::Regex;
3use std::sync::LazyLock;
4use std::{fs, str::FromStr};
5
6use crate::config::ArrowKeyBindings;
7use crate::displayed_item_tree::{Node, VisibleItemIndex};
8use crate::fzcmd::{Command, ParamGreed};
9use crate::hierarchy::HierarchyStyle;
10use crate::message::MessageTarget;
11use crate::transaction_container::StreamScopeRef;
12use crate::wave_container::{ScopeRef, ScopeRefExt, VariableRef, VariableRefExt};
13use crate::wave_data::ScopeType;
14use crate::wave_source::LoadOptions;
15use crate::{
16 SystemState,
17 clock_highlighting::ClockHighlightType,
18 displayed_item::DisplayedItem,
19 message::Message,
20 util::{alpha_idx_to_uint_idx, uint_idx_to_alpha_idx},
21 variable_name_type::VariableNameType,
22};
23use itertools::Itertools;
24use tracing::warn;
25
26type RestCommand = Box<dyn Fn(&str) -> Option<Command<Message>>>;
27
28fn is_wave_file_extension(ext: &str) -> bool {
30 matches!(ext, "vcd" | "fst" | "ghw")
31}
32
33fn is_command_file_extension(ext: &str) -> bool {
35 matches!(ext, "sucl")
36}
37
38fn separate_at_space(query: &str) -> (String, String, String, String) {
43 static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(\s*)(\S*)(\s?)(.*)").unwrap());
44
45 let captures = RE.captures_iter(query).next().unwrap();
46
47 (
48 captures[1].into(),
49 captures[2].into(),
50 captures[3].into(),
51 captures[4].into(),
52 )
53}
54
55pub fn get_parser(state: &SystemState) -> Command<Message> {
56 fn single_word(
57 suggestions: Vec<String>,
58 rest_command: RestCommand,
59 ) -> Option<Command<Message>> {
60 Some(Command::NonTerminal(
61 ParamGreed::Rest,
62 suggestions,
63 Box::new(move |query, _| rest_command(query)),
64 ))
65 }
66
67 fn optional_single_word(
68 suggestions: Vec<String>,
69 rest_command: RestCommand,
70 ) -> Option<Command<Message>> {
71 Some(Command::NonTerminal(
72 ParamGreed::OptionalWord,
73 suggestions,
74 Box::new(move |query, _| rest_command(query)),
75 ))
76 }
77
78 fn single_word_delayed_suggestions(
79 suggestions: Box<dyn Fn() -> Vec<String>>,
80 rest_command: RestCommand,
81 ) -> Option<Command<Message>> {
82 Some(Command::NonTerminal(
83 ParamGreed::Rest,
84 suggestions(),
85 Box::new(move |query, _| rest_command(query)),
86 ))
87 }
88
89 let scopes = match &state.user.waves {
90 Some(v) => v.inner.scope_names(),
91 None => vec![],
92 };
93 let variables = match &state.user.waves {
94 Some(v) => v.inner.variable_names(),
95 None => vec![],
96 };
97 let surver_file_names = state
98 .user
99 .surver_file_infos
100 .as_ref()
101 .map_or(vec![], |file_infos| {
102 file_infos
103 .iter()
104 .map(|info| info.filename.clone())
105 .collect()
106 });
107 let displayed_items = match &state.user.waves {
108 Some(v) => v
109 .items_tree
110 .iter_visible()
111 .enumerate()
112 .map(
113 |(
114 vidx,
115 Node {
116 item_ref: item_id, ..
117 },
118 )| {
119 let idx = VisibleItemIndex(vidx);
120 let item = &v.displayed_items[item_id];
121 match item {
122 DisplayedItem::Variable(var) => format!(
123 "{}_{}",
124 uint_idx_to_alpha_idx(idx, v.displayed_items.len()),
125 var.variable_ref.full_path_string()
126 ),
127 _ => format!(
128 "{}_{}",
129 uint_idx_to_alpha_idx(idx, v.displayed_items.len()),
130 item.name()
131 ),
132 }
133 },
134 )
135 .collect_vec(),
136 None => vec![],
137 };
138 let variables_in_active_scope = state
139 .user
140 .waves
141 .as_ref()
142 .and_then(|waves| {
143 waves
144 .active_scope
145 .as_ref()
146 .map(|scope| waves.inner.variables_in_scope(scope))
147 })
148 .unwrap_or_default();
149
150 let color_names = state.user.config.theme.colors.keys().cloned().collect_vec();
151 let format_names: Vec<String> = state
152 .translators
153 .all_translator_names()
154 .into_iter()
155 .map(&str::to_owned)
156 .collect();
157
158 let active_scope = state
159 .user
160 .waves
161 .as_ref()
162 .and_then(|w| w.active_scope.clone());
163
164 let is_transaction_container = state
165 .user
166 .waves
167 .as_ref()
168 .is_some_and(|w| w.inner.is_transactions());
169
170 fn files_with_ext(matches: fn(&str) -> bool) -> Vec<String> {
171 if let Ok(res) = fs::read_dir(".") {
172 res.map(|res| res.map(|e| e.path()).unwrap_or_default())
173 .filter(|file| {
174 file.extension()
175 .is_some_and(|extension| (matches)(extension.to_str().unwrap_or("")))
176 })
177 .map(|file| file.into_os_string().into_string().unwrap())
178 .collect::<Vec<String>>()
179 } else {
180 vec![]
181 }
182 }
183
184 fn all_wave_files() -> Vec<String> {
185 files_with_ext(is_wave_file_extension)
186 }
187
188 fn all_command_files() -> Vec<String> {
189 files_with_ext(is_command_file_extension)
190 }
191
192 let markers = if let Some(waves) = &state.user.waves {
193 waves
194 .items_tree
195 .iter()
196 .map(|Node { item_ref, .. }| waves.displayed_items.get(item_ref))
197 .filter_map(|item| match item {
198 Some(DisplayedItem::Marker(marker)) => Some((marker.name.clone(), marker.idx)),
199 _ => None,
200 })
201 .collect::<Vec<_>>()
202 } else {
203 Vec::new()
204 };
205
206 fn parse_marker(query: &str, markers: &[(Option<String>, u8)]) -> Option<u8> {
207 if let Some(id_str) = query.strip_prefix("#") {
208 let id = id_str.parse::<u8>().ok()?;
209 Some(id)
210 } else {
211 markers
212 .iter()
213 .find_map(|(name, idx)| name.as_ref().and_then(|n| (n == query).then_some(*idx)))
214 }
215 }
216
217 fn marker_suggestions(markers: &[(Option<String>, u8)]) -> Vec<String> {
218 markers
219 .iter()
220 .flat_map(|(name, idx)| {
221 [name.clone(), Some(format!("#{idx}"))]
222 .into_iter()
223 .flatten()
224 })
225 .collect()
226 }
227
228 let wcp_start_or_stop = if state
229 .wcp_running_signal
230 .load(std::sync::atomic::Ordering::Relaxed)
231 {
232 "wcp_server_stop"
233 } else {
234 "wcp_server_start"
235 };
236 #[cfg(target_arch = "wasm32")]
237 let _ = wcp_start_or_stop;
238
239 let keep_during_reload = state.user.config.behavior.keep_during_reload;
240 let mut commands = if state.user.waves.is_some() {
241 vec![
242 "load_file",
243 "load_url",
244 #[cfg(not(target_arch = "wasm32"))]
245 "load_state",
246 "run_command_file",
247 "run_command_file_from_url",
248 "switch_file",
249 "variable_add",
250 "generator_add",
251 "item_focus",
252 "item_set_color",
253 "item_set_background_color",
254 "item_set_format",
255 "item_unset_color",
256 "item_unset_background_color",
257 "item_unfocus",
258 "item_rename",
259 "zoom_fit",
260 "scope_add",
261 "scope_add_recursive",
262 "scope_add_as_group",
263 "scope_add_as_group_recursive",
264 "scope_select",
265 "scope_select_root",
266 "stream_add",
267 "stream_select",
268 "stream_select_root",
269 "divider_add",
270 "config_reload",
271 "theme_select",
272 "reload",
273 "remove_unavailable",
274 "show_controls",
275 "show_mouse_gestures",
276 "show_quick_start",
277 "show_logs",
278 #[cfg(feature = "performance_plot")]
279 "show_performance",
280 "scroll_to_start",
281 "scroll_to_end",
282 "goto_start",
283 "goto_end",
284 "zoom_in",
285 "zoom_out",
286 "toggle_menu",
287 "toggle_side_panel",
288 "toggle_fullscreen",
289 "toggle_tick_lines",
290 "variable_add_from_scope",
291 "generator_add_from_stream",
292 "variable_set_name_type",
293 "variable_force_name_type",
294 "preference_set_clock_highlight",
295 "preference_set_hierarchy_style",
296 "preference_set_arrow_key_bindings",
297 "goto_cursor",
298 "goto_marker",
299 "dump_tree",
300 "group_marked",
301 "group_dissolve",
302 "group_fold_recursive",
303 "group_unfold_recursive",
304 "group_fold_all",
305 "group_unfold_all",
306 "save_state",
307 "save_state_as",
308 "timeline_add",
309 "cursor_set",
310 "marker_set",
311 "marker_remove",
312 "show_marker_window",
313 "viewport_add",
314 "viewport_remove",
315 "transition_next",
316 "transition_previous",
317 "transaction_next",
318 "transaction_prev",
319 "copy_value",
320 "pause_simulation",
321 "unpause_simulation",
322 "undo",
323 "redo",
324 #[cfg(not(target_arch = "wasm32"))]
325 wcp_start_or_stop,
326 #[cfg(not(target_arch = "wasm32"))]
327 "exit",
328 ]
329 } else {
330 vec![
331 "load_file",
332 "load_url",
333 #[cfg(not(target_arch = "wasm32"))]
334 "load_state",
335 "run_command_file",
336 "run_command_file_from_url",
337 "config_reload",
338 "theme_select",
339 "toggle_menu",
340 "toggle_side_panel",
341 "toggle_fullscreen",
342 "preference_set_clock_highlight",
343 "preference_set_hierarchy_style",
344 "preference_set_arrow_key_bindings",
345 "show_controls",
346 "show_mouse_gestures",
347 "show_quick_start",
348 "show_logs",
349 #[cfg(feature = "performance_plot")]
350 "show_performance",
351 #[cfg(not(target_arch = "wasm32"))]
352 wcp_start_or_stop,
353 #[cfg(not(target_arch = "wasm32"))]
354 "exit",
355 ]
356 };
357 if !surver_file_names.is_empty() {
358 commands.push("surver_select_file");
359 commands.push("surver_switch_file");
360 }
361
362 let mut theme_names = state.user.config.theme.theme_names.clone();
363 let state_file = state.user.state_file.clone();
364 let show_hierarchy = state.show_hierarchy();
365 let show_menu = state.show_menu();
366 let show_tick_lines = state.show_ticks();
367 theme_names.insert(0, "default".to_string());
368 Command::NonTerminal(
369 ParamGreed::Word,
370 commands.into_iter().map(std::convert::Into::into).collect(),
371 Box::new(move |query, _| {
372 let variables_in_active_scope = variables_in_active_scope.clone();
373 let markers = markers.clone();
374 let scopes = scopes.clone();
375 let active_scope = active_scope.clone();
376 let is_transaction_container = is_transaction_container;
377 match query {
378 "load_file" => single_word_delayed_suggestions(
379 Box::new(all_wave_files),
380 Box::new(|word| {
381 Some(Command::Terminal(Message::LoadFile(
382 word.into(),
383 LoadOptions::Clear,
384 )))
385 }),
386 ),
387 "switch_file" => single_word_delayed_suggestions(
388 Box::new(all_wave_files),
389 Box::new(|word| {
390 Some(Command::Terminal(Message::LoadFile(
391 word.into(),
392 LoadOptions::KeepAll,
393 )))
394 }),
395 ),
396 "load_url" => Some(Command::NonTerminal(
397 ParamGreed::Rest,
398 vec![],
399 Box::new(|query, _| {
400 Some(Command::Terminal(Message::LoadWaveformFileFromUrl(
401 query.to_string(),
402 LoadOptions::Clear, )))
404 }),
405 )),
406 "run_command_file" => single_word_delayed_suggestions(
407 Box::new(all_command_files),
408 Box::new(|word| Some(Command::Terminal(Message::LoadCommandFile(word.into())))),
409 ),
410 "run_command_file_from_url" => Some(Command::NonTerminal(
411 ParamGreed::Rest,
412 vec![],
413 Box::new(|query, _| {
414 Some(Command::Terminal(Message::LoadCommandFileFromUrl(
415 query.to_string(),
416 )))
417 }),
418 )),
419 "config_reload" => Some(Command::Terminal(Message::ReloadConfig)),
420 "theme_select" => single_word(
421 theme_names.clone(),
422 Box::new(|word| {
423 Some(Command::Terminal(Message::SelectTheme(Some(
424 word.to_owned(),
425 ))))
426 }),
427 ),
428 "scroll_to_start" | "goto_start" => {
429 Some(Command::Terminal(Message::GoToStart { viewport_idx: 0 }))
430 }
431 "scroll_to_end" | "goto_end" => {
432 Some(Command::Terminal(Message::GoToEnd { viewport_idx: 0 }))
433 }
434 "zoom_in" => Some(Command::Terminal(Message::CanvasZoom {
435 mouse_ptr: None,
436 delta: 0.5,
437 viewport_idx: 0,
438 })),
439 "zoom_out" => Some(Command::Terminal(Message::CanvasZoom {
440 mouse_ptr: None,
441 delta: 2.0,
442 viewport_idx: 0,
443 })),
444 "zoom_fit" => Some(Command::Terminal(Message::ZoomToFit { viewport_idx: 0 })),
445 "toggle_menu" => Some(Command::Terminal(Message::SetMenuVisible(!show_menu))),
446 "toggle_side_panel" => Some(Command::Terminal(Message::SetSidePanelVisible(
447 !show_hierarchy,
448 ))),
449 "toggle_fullscreen" => Some(Command::Terminal(Message::ToggleFullscreen)),
450 "toggle_tick_lines" => {
451 Some(Command::Terminal(Message::SetTickLines(!show_tick_lines)))
452 }
453 "scope_add" | "module_add" | "stream_add" | "scope_add_recursive" => {
455 let recursive = query == "scope_add_recursive";
456 if is_transaction_container {
457 if recursive {
458 warn!("Cannot recursively add transaction containers");
459 }
460 single_word(
461 scopes,
462 Box::new(|word| {
463 Some(Command::Terminal(Message::AddAllFromStreamScope(
464 word.to_string(),
465 )))
466 }),
467 )
468 } else {
469 single_word(
470 scopes,
471 Box::new(move |word| {
472 Some(Command::Terminal(Message::AddScope(
473 ScopeRef::from_hierarchy_string(word),
474 recursive,
475 )))
476 }),
477 )
478 }
479 }
480 "scope_add_as_group" | "scope_add_as_group_recursive" => {
481 let recursive = query == "scope_add_as_group_recursive";
482 if is_transaction_container {
483 warn!("Cannot add transaction containers as group");
484 None
485 } else {
486 single_word(
487 scopes,
488 Box::new(move |word| {
489 Some(Command::Terminal(Message::AddScopeAsGroup(
490 ScopeRef::from_hierarchy_string(word),
491 recursive,
492 )))
493 }),
494 )
495 }
496 }
497 "scope_select" | "stream_select" => {
498 if is_transaction_container {
499 single_word(
500 scopes.clone(),
501 Box::new(|word| {
502 let scope = if word == "tr" {
503 ScopeType::StreamScope(StreamScopeRef::Root)
504 } else {
505 ScopeType::StreamScope(StreamScopeRef::Empty(word.to_string()))
506 };
507 Some(Command::Terminal(Message::SetActiveScope(Some(scope))))
508 }),
509 )
510 } else {
511 single_word(
512 scopes.clone(),
513 Box::new(|word| {
514 Some(Command::Terminal(Message::SetActiveScope(Some(
515 ScopeType::WaveScope(ScopeRef::from_hierarchy_string(word)),
516 ))))
517 }),
518 )
519 }
520 }
521 "scope_select_root" | "stream_select_root" => {
522 Some(Command::Terminal(Message::SetActiveScope(None)))
523 }
524 "reload" => Some(Command::Terminal(Message::ReloadWaveform(
525 keep_during_reload,
526 ))),
527 "remove_unavailable" => Some(Command::Terminal(Message::RemovePlaceholders)),
528 "surver_select_file" => single_word(
529 surver_file_names.clone(),
530 Box::new(|word| {
531 Some(Command::Terminal(Message::LoadSurverFileByName(
532 word.to_string(),
533 LoadOptions::Clear,
534 )))
535 }),
536 ),
537 "surver_switch_file" => single_word(
538 surver_file_names.clone(),
539 Box::new(|word| {
540 Some(Command::Terminal(Message::LoadSurverFileByName(
541 word.to_string(),
542 LoadOptions::KeepAll,
543 )))
544 }),
545 ),
546 "variable_add" | "generator_add" => {
548 if is_transaction_container {
549 single_word(
550 variables.clone(),
551 Box::new(|word| {
552 Some(Command::Terminal(Message::AddStreamOrGeneratorFromName(
553 None,
554 word.to_string(),
555 )))
556 }),
557 )
558 } else {
559 single_word(
560 variables.clone(),
561 Box::new(|word| {
562 Some(Command::Terminal(Message::AddVariables(vec![
563 VariableRef::from_hierarchy_string(word),
564 ])))
565 }),
566 )
567 }
568 }
569 "variable_add_from_scope" | "generator_add_from_stream" => single_word(
570 variables_in_active_scope
571 .into_iter()
572 .map(|s| s.name())
573 .collect(),
574 Box::new(move |name| {
575 active_scope.as_ref().map(|scope| match scope {
576 ScopeType::WaveScope(w) => Command::Terminal(Message::AddVariables(
577 vec![VariableRef::new(w.clone(), name.to_string())],
578 )),
579 ScopeType::StreamScope(stream_scope) => {
580 Command::Terminal(Message::AddStreamOrGeneratorFromName(
581 Some(stream_scope.clone()),
582 name.to_string(),
583 ))
584 }
585 })
586 }),
587 ),
588 "item_set_color" => single_word(
589 color_names.clone(),
590 Box::new(|word| {
591 Some(Command::Terminal(Message::ItemColorChange(
592 MessageTarget::CurrentSelection,
593 Some(word.to_string()),
594 )))
595 }),
596 ),
597 "item_set_background_color" => single_word(
598 color_names.clone(),
599 Box::new(|word| {
600 Some(Command::Terminal(Message::ItemBackgroundColorChange(
601 MessageTarget::CurrentSelection,
602 Some(word.to_string()),
603 )))
604 }),
605 ),
606 "item_unset_color" => Some(Command::Terminal(Message::ItemColorChange(
607 MessageTarget::CurrentSelection,
608 None,
609 ))),
610 "item_set_format" => single_word(
611 format_names.clone(),
612 Box::new(|word| {
613 Some(Command::Terminal(Message::VariableFormatChange(
614 MessageTarget::CurrentSelection,
615 word.to_string(),
616 )))
617 }),
618 ),
619 "item_unset_background_color" => Some(Command::Terminal(
620 Message::ItemBackgroundColorChange(MessageTarget::CurrentSelection, None),
621 )),
622 "item_rename" => Some(Command::NonTerminal(
623 ParamGreed::Rest,
624 vec![],
625 Box::new(|query, _| {
626 Some(Command::Terminal(Message::ItemNameChange(
627 None,
628 Some(query.to_owned()),
629 )))
630 }),
631 )),
632 "variable_set_name_type" => single_word(
633 vec![
634 "Local".to_string(),
635 "Unique".to_string(),
636 "Global".to_string(),
637 ],
638 Box::new(|word| {
639 Some(Command::Terminal(Message::ChangeVariableNameType(
640 MessageTarget::CurrentSelection,
641 VariableNameType::from_str(word).unwrap_or(VariableNameType::Local),
642 )))
643 }),
644 ),
645 "variable_force_name_type" => single_word(
646 vec![
647 "Local".to_string(),
648 "Unique".to_string(),
649 "Global".to_string(),
650 ],
651 Box::new(|word| {
652 Some(Command::Terminal(Message::ForceVariableNameTypes(
653 VariableNameType::from_str(word).unwrap_or(VariableNameType::Local),
654 )))
655 }),
656 ),
657 "item_focus" => single_word(
658 displayed_items.clone(),
659 Box::new(|word| {
660 let alpha_idx: String = word.chars().take_while(|c| *c != '_').collect();
662 alpha_idx_to_uint_idx(&alpha_idx)
663 .map(|idx| Command::Terminal(Message::FocusItem(idx)))
664 }),
665 ),
666 "transition_next" => single_word(
667 displayed_items.clone(),
668 Box::new(|word| {
669 let alpha_idx: String = word.chars().take_while(|c| *c != '_').collect();
671 alpha_idx_to_uint_idx(&alpha_idx).map(|idx| {
672 Command::Terminal(Message::MoveCursorToTransition {
673 next: true,
674 variable: Some(idx),
675 skip_zero: false,
676 })
677 })
678 }),
679 ),
680 "transition_previous" => single_word(
681 displayed_items.clone(),
682 Box::new(|word| {
683 let alpha_idx: String = word.chars().take_while(|c| *c != '_').collect();
685 alpha_idx_to_uint_idx(&alpha_idx).map(|idx| {
686 Command::Terminal(Message::MoveCursorToTransition {
687 next: false,
688 variable: Some(idx),
689 skip_zero: false,
690 })
691 })
692 }),
693 ),
694 "transaction_next" => {
695 Some(Command::Terminal(Message::MoveTransaction { next: true }))
696 }
697 "transaction_prev" => {
698 Some(Command::Terminal(Message::MoveTransaction { next: false }))
699 }
700 "copy_value" => single_word(
701 displayed_items.clone(),
702 Box::new(|word| {
703 let alpha_idx: String = word.chars().take_while(|c| *c != '_').collect();
705 alpha_idx_to_uint_idx(&alpha_idx).map(|idx| {
706 Command::Terminal(Message::VariableValueToClipbord(
707 MessageTarget::Explicit(idx),
708 ))
709 })
710 }),
711 ),
712 "preference_set_clock_highlight" => single_word(
713 ["Line", "Cycle", "None"]
714 .iter()
715 .map(ToString::to_string)
716 .collect_vec(),
717 Box::new(|word| {
718 Some(Command::Terminal(Message::SetClockHighlightType(
719 ClockHighlightType::from_str(word).unwrap_or(ClockHighlightType::Line),
720 )))
721 }),
722 ),
723 "preference_set_hierarchy_style" => single_word(
724 enum_iterator::all::<HierarchyStyle>()
725 .map(|o| o.to_string())
726 .collect_vec(),
727 Box::new(|word| {
728 Some(Command::Terminal(Message::SetHierarchyStyle(
729 HierarchyStyle::from_str(word).unwrap_or(HierarchyStyle::Separate),
730 )))
731 }),
732 ),
733 "preference_set_arrow_key_bindings" => single_word(
734 enum_iterator::all::<ArrowKeyBindings>()
735 .map(|o| o.to_string())
736 .collect_vec(),
737 Box::new(|word| {
738 Some(Command::Terminal(Message::SetArrowKeyBindings(
739 ArrowKeyBindings::from_str(word).unwrap_or(ArrowKeyBindings::Edge),
740 )))
741 }),
742 ),
743 "item_unfocus" => Some(Command::Terminal(Message::UnfocusItem)),
744 "divider_add" => optional_single_word(
745 vec![],
746 Box::new(|word| {
747 Some(Command::Terminal(Message::AddDivider(
748 Some(word.into()),
749 None,
750 )))
751 }),
752 ),
753 "timeline_add" => Some(Command::Terminal(Message::AddTimeLine(None))),
754 "goto_cursor" => Some(Command::Terminal(Message::GoToCursorIfNotInView)),
755 "goto_marker" => single_word(
756 marker_suggestions(&markers),
757 Box::new(move |name| {
758 parse_marker(name, &markers)
759 .map(|idx| Command::Terminal(Message::GoToMarkerPosition(idx, 0)))
760 }),
761 ),
762 "dump_tree" => Some(Command::Terminal(Message::DumpTree)),
763 "group_marked" => optional_single_word(
764 vec![],
765 Box::new(|name| {
766 let trimmed = name.trim();
767 Some(Command::Terminal(Message::GroupNew {
768 name: (!trimmed.is_empty()).then_some(trimmed.to_owned()),
769 before: None,
770 items: None,
771 }))
772 }),
773 ),
774 "group_dissolve" => Some(Command::Terminal(Message::GroupDissolve(None))),
775 "group_fold_recursive" => {
776 Some(Command::Terminal(Message::GroupFoldRecursive(None)))
777 }
778 "group_unfold_recursive" => {
779 Some(Command::Terminal(Message::GroupUnfoldRecursive(None)))
780 }
781 "group_fold_all" => Some(Command::Terminal(Message::GroupFoldAll)),
782 "group_unfold_all" => Some(Command::Terminal(Message::GroupUnfoldAll)),
783 "show_controls" => Some(Command::Terminal(Message::SetKeyHelpVisible(true))),
784 "show_mouse_gestures" => {
785 Some(Command::Terminal(Message::SetGestureHelpVisible(true)))
786 }
787 "show_quick_start" => Some(Command::Terminal(Message::SetQuickStartVisible(true))),
788 #[cfg(feature = "performance_plot")]
789 "show_performance" => optional_single_word(
790 vec![],
791 Box::new(|word| {
792 if word == "redraw" {
793 Some(Command::Terminal(Message::Batch(vec![
794 Message::SetPerformanceVisible(true),
795 Message::SetContinuousRedraw(true),
796 ])))
797 } else {
798 Some(Command::Terminal(Message::SetPerformanceVisible(true)))
799 }
800 }),
801 ),
802 "cursor_set" => single_word(
803 vec![],
804 Box::new(|time_str| match time_str.parse() {
805 Ok(time) => Some(Command::Terminal(Message::Batch(vec![
806 Message::CursorSet(time),
807 Message::GoToCursorIfNotInView,
808 ]))),
809 _ => None,
810 }),
811 ),
812 "marker_set" => Some(Command::NonTerminal(
813 ParamGreed::Custom(&separate_at_space),
814 vec![],
817 Box::new(move |name, _| {
818 let marker_id = parse_marker(name, &markers);
819 let name = name.to_owned();
820
821 Some(Command::NonTerminal(
822 ParamGreed::Word,
823 vec![],
824 Box::new(move |time_str, _| {
825 let time = time_str.parse().ok()?;
826 match marker_id {
827 Some(id) => {
828 Some(Command::Terminal(Message::SetMarker { id, time }))
829 }
830 None => Some(Command::Terminal(Message::AddMarker {
831 time,
832 name: Some(name.clone()),
833 move_focus: true,
834 })),
835 }
836 }),
837 ))
838 }),
839 )),
840 "marker_remove" => Some(Command::NonTerminal(
841 ParamGreed::Rest,
842 marker_suggestions(&markers),
843 Box::new(move |name, _| {
844 let marker_id = parse_marker(name, &markers)?;
845 Some(Command::Terminal(Message::RemoveMarker(marker_id)))
846 }),
847 )),
848 "show_marker_window" => {
849 Some(Command::Terminal(Message::SetCursorWindowVisible(true)))
850 }
851 "show_logs" => Some(Command::Terminal(Message::SetLogsVisible(true))),
852 "save_state" => Some(Command::Terminal(Message::SaveStateFile(
853 state_file.clone(),
854 ))),
855 "save_state_as" => single_word(
856 vec![],
857 Box::new(|word| {
858 Some(Command::Terminal(Message::SaveStateFile(Some(
859 std::path::Path::new(word).into(),
860 ))))
861 }),
862 ),
863 "load_state" => single_word(
864 vec![],
865 Box::new(|word| {
866 Some(Command::Terminal(Message::LoadStateFile(Some(
867 std::path::Path::new(word).into(),
868 ))))
869 }),
870 ),
871 "viewport_add" => Some(Command::Terminal(Message::AddViewport)),
872 "viewport_remove" => Some(Command::Terminal(Message::RemoveViewport)),
873 "pause_simulation" => Some(Command::Terminal(Message::PauseSimulation)),
874 "unpause_simulation" => Some(Command::Terminal(Message::UnpauseSimulation)),
875 "undo" => Some(Command::Terminal(Message::Undo(1))),
876 "redo" => Some(Command::Terminal(Message::Redo(1))),
877 "wcp_server_start" => Some(Command::Terminal(Message::StartWcpServer {
878 address: None,
879 initiate: false,
880 })),
881 "wcp_server_stop" => Some(Command::Terminal(Message::StopWcpServer)),
882 "exit" => Some(Command::Terminal(Message::Exit)),
883 _ => None,
884 }
885 }),
886 )
887}