Skip to main content

libsurfer/
command_parser.rs

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