libsurfer/
command_parser.rs

1//! Command prompt handling.
2use 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
27/// Match str with wave file extensions, currently: vcd, fst, ghw
28fn is_wave_file_extension(ext: &str) -> bool {
29    ext == "vcd" || ext == "fst" || ext == "ghw"
30}
31
32/// Match str with command file extensions, currently: sucl
33fn is_command_file_extension(ext: &str) -> bool {
34    ext == "sucl"
35}
36
37/// Split part of a query at whitespace
38///
39/// fzcmd splits at regex "words" which does not include special characters
40/// like '#'. This function can be used instead via `ParamGreed::Custom(&separate_at_space)`
41fn 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(), // load_url does not indicate any format restrictions
392                        )))
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 commands
439                "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 commands
511                "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                        // split off the idx which is always followed by an underscore
616                        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                        // split off the idx which is always followed by an underscore
625                        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                        // split off the idx which is always followed by an underscore
639                        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                        // split off the idx which is always followed by an underscore
659                        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                    // FIXME use once fzcmd does not enforce suggestion match, as of now we couldn't add a marker (except the first)
770                    // marker_suggestions(&markers),
771                    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}