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