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