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