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::frame_buffer::FrameBufferColorMode;
9use crate::fzcmd::{Command, ParamGreed};
10use crate::hierarchy::HierarchyStyle;
11use crate::message::MessageTarget;
12use crate::transaction_container::StreamScopeRef;
13use crate::wave_container::{ScopeRef, ScopeRefExt, VariableRef, VariableRefExt};
14use crate::wave_data::ScopeType;
15use crate::wave_source::LoadOptions;
16use crate::{
17    SystemState,
18    clock_highlighting::ClockHighlightType,
19    displayed_item::DisplayedItem,
20    message::Message,
21    util::{alpha_idx_to_uint_idx, uint_idx_to_alpha_idx},
22    variable_name_type::VariableNameType,
23};
24use itertools::Itertools;
25use tracing::warn;
26
27type RestCommand = Box<dyn Fn(&str) -> Option<Command<Message>>>;
28
29/// Match str with wave file extensions, currently: vcd, fst, ghw
30fn is_wave_file_extension(ext: &str) -> bool {
31    matches!(ext, "vcd" | "fst" | "ghw")
32}
33
34/// Match str with command file extensions, currently: sucl
35fn is_command_file_extension(ext: &str) -> bool {
36    matches!(ext, "sucl")
37}
38
39/// Split part of a query at whitespace
40///
41/// fzcmd splits at regex "words" which does not include special characters
42/// like '#'. This function can be used instead via `ParamGreed::Custom(&separate_at_space)`
43fn separate_at_space(query: &str) -> (String, String, String, String) {
44    static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(\s*)(\S*)(\s?)(.*)").unwrap());
45
46    let captures = RE.captures_iter(query).next().unwrap();
47
48    (
49        captures[1].into(),
50        captures[2].into(),
51        captures[3].into(),
52        captures[4].into(),
53    )
54}
55
56pub(crate) fn get_parser(state: &SystemState) -> Command<Message> {
57    fn single_word(
58        suggestions: Vec<String>,
59        rest_command: RestCommand,
60    ) -> Option<Command<Message>> {
61        Some(Command::NonTerminal(
62            ParamGreed::Rest,
63            suggestions,
64            Box::new(move |query, _| rest_command(query)),
65        ))
66    }
67
68    fn optional_single_word(
69        suggestions: Vec<String>,
70        rest_command: RestCommand,
71    ) -> Option<Command<Message>> {
72        Some(Command::NonTerminal(
73            ParamGreed::OptionalWord,
74            suggestions,
75            Box::new(move |query, _| rest_command(query)),
76        ))
77    }
78
79    fn single_word_delayed_suggestions(
80        suggestions: Box<dyn Fn() -> Vec<String>>,
81        rest_command: RestCommand,
82    ) -> Option<Command<Message>> {
83        Some(Command::NonTerminal(
84            ParamGreed::Rest,
85            suggestions(),
86            Box::new(move |query, _| rest_command(query)),
87        ))
88    }
89
90    let scopes = match &state.user.waves {
91        Some(v) => v.inner.scope_names(),
92        None => vec![],
93    };
94    let variables = match &state.user.waves {
95        Some(v) => v.inner.variable_names(),
96        None => vec![],
97    };
98    let arrays = match &state.user.waves {
99        Some(v) => v.inner.array_names(),
100        None => vec![],
101    };
102    let surver_file_names = state
103        .user
104        .surver_file_infos
105        .as_ref()
106        .map_or(vec![], |file_infos| {
107            file_infos
108                .iter()
109                .map(|info| info.filename.clone())
110                .collect()
111        });
112    let displayed_items = match &state.user.waves {
113        Some(v) => v
114            .items_tree
115            .iter_visible()
116            .enumerate()
117            .map(
118                |(
119                    vidx,
120                    Node {
121                        item_ref: item_id, ..
122                    },
123                )| {
124                    let idx = VisibleItemIndex(vidx);
125                    let item = &v.displayed_items[item_id];
126                    match item {
127                        DisplayedItem::Variable(var) => format!(
128                            "{}_{}",
129                            uint_idx_to_alpha_idx(idx, v.displayed_items.len()),
130                            var.variable_ref.full_path_string()
131                        ),
132                        _ => format!(
133                            "{}_{}",
134                            uint_idx_to_alpha_idx(idx, v.displayed_items.len()),
135                            item.name()
136                        ),
137                    }
138                },
139            )
140            .collect_vec(),
141        None => vec![],
142    };
143    let variables_in_active_scope = state
144        .user
145        .waves
146        .as_ref()
147        .and_then(|waves| {
148            waves
149                .active_scope
150                .as_ref()
151                .map(|scope| waves.inner.variables_in_scope(scope))
152        })
153        .unwrap_or_default();
154
155    let color_names = state.user.config.theme.colors.keys().cloned().collect_vec();
156    let format_names: Vec<String> = state
157        .translators
158        .all_translator_names()
159        .into_iter()
160        .map(&str::to_owned)
161        .collect();
162
163    let active_scope = state
164        .user
165        .waves
166        .as_ref()
167        .and_then(|w| w.active_scope.clone());
168
169    let is_transaction_container = state
170        .user
171        .waves
172        .as_ref()
173        .is_some_and(|w| w.inner.is_transactions());
174
175    fn files_with_ext(matches: fn(&str) -> bool) -> Vec<String> {
176        if let Ok(res) = fs::read_dir(".") {
177            res.map(|res| res.map(|e| e.path()).unwrap_or_default())
178                .filter(|file| {
179                    file.extension()
180                        .is_some_and(|extension| (matches)(extension.to_str().unwrap_or("")))
181                })
182                .map(|file| file.into_os_string().into_string().unwrap())
183                .collect::<Vec<String>>()
184        } else {
185            vec![]
186        }
187    }
188
189    fn all_wave_files() -> Vec<String> {
190        files_with_ext(is_wave_file_extension)
191    }
192
193    fn all_command_files() -> Vec<String> {
194        files_with_ext(is_command_file_extension)
195    }
196
197    let timescale = state
198        .user
199        .waves
200        .as_ref()
201        .map(|w| w.inner.metadata().timescale.clone());
202
203    let markers = if let Some(waves) = &state.user.waves {
204        waves
205            .items_tree
206            .iter()
207            .map(|Node { item_ref, .. }| waves.displayed_items.get(item_ref))
208            .filter_map(|item| match item {
209                Some(DisplayedItem::Marker(marker)) => Some((marker.name.clone(), marker.idx)),
210                _ => None,
211            })
212            .collect::<Vec<_>>()
213    } else {
214        Vec::new()
215    };
216
217    fn parse_marker(query: &str, markers: &[(Option<String>, u8)]) -> Option<u8> {
218        if let Some(id_str) = query.strip_prefix("#") {
219            let id = id_str.parse::<u8>().ok()?;
220            Some(id)
221        } else {
222            markers
223                .iter()
224                .find_map(|(name, idx)| name.as_ref().and_then(|n| (n == query).then_some(*idx)))
225        }
226    }
227
228    fn marker_suggestions(markers: &[(Option<String>, u8)]) -> Vec<String> {
229        markers
230            .iter()
231            .flat_map(|(name, idx)| {
232                [name.clone(), Some(format!("#{idx}"))]
233                    .into_iter()
234                    .flatten()
235            })
236            .collect()
237    }
238
239    let wcp_start_or_stop = if state
240        .wcp_running_signal
241        .load(std::sync::atomic::Ordering::Relaxed)
242    {
243        "wcp_server_stop"
244    } else {
245        "wcp_server_start"
246    };
247    #[cfg(target_arch = "wasm32")]
248    let _ = wcp_start_or_stop;
249
250    let keep_during_reload = state.user.config.behavior.keep_during_reload;
251    let mut commands = if state.user.waves.is_some() {
252        vec![
253            "load_file",
254            "load_url",
255            #[cfg(not(target_arch = "wasm32"))]
256            "load_state",
257            "run_command_file",
258            "run_command_file_from_url",
259            "switch_file",
260            "variable_add",
261            "generator_add",
262            "item_focus",
263            "item_set_color",
264            "item_set_background_color",
265            "item_set_format",
266            "item_unset_color",
267            "item_unset_background_color",
268            "item_unfocus",
269            "item_rename",
270            "zoom_fit",
271            "scope_add",
272            #[cfg(not(target_arch = "wasm32"))]
273            "create_default_config",
274            "scope_add_recursive",
275            "scope_add_as_group",
276            "scope_add_as_group_recursive",
277            "scope_select",
278            "scope_select_root",
279            "stream_add",
280            "stream_select",
281            "stream_select_root",
282            "divider_add",
283            "config_reload",
284            "theme_select",
285            "reload",
286            "remove_unavailable",
287            "show_controls",
288            "show_mouse_gestures",
289            "show_quick_start",
290            "show_logs",
291            #[cfg(feature = "performance_plot")]
292            "show_performance",
293            "scroll_to_start",
294            "scroll_to_end",
295            "goto_start",
296            "goto_end",
297            "zoom_in",
298            "zoom_out",
299            "zoom_to",
300            "toggle_menu",
301            "toggle_side_panel",
302            "toggle_fullscreen",
303            "toggle_tick_lines",
304            "variable_add_from_scope",
305            "generator_add_from_stream",
306            "variable_set_name_type",
307            "variable_force_name_type",
308            "preference_set_clock_highlight",
309            "preference_set_hierarchy_style",
310            "preference_set_arrow_key_bindings",
311            "goto_cursor",
312            "goto_marker",
313            "dump_tree",
314            "group_marked",
315            "group_dissolve",
316            "group_fold_recursive",
317            "group_unfold_recursive",
318            "group_fold_all",
319            "group_unfold_all",
320            "save_state",
321            "save_state_as",
322            "timeline_add",
323            "cursor_set",
324            "goto_time",
325            "marker_set",
326            "marker_set_at",
327            "marker_remove",
328            "show_marker_window",
329            "viewport_add",
330            "viewport_remove",
331            "transition_next",
332            "transition_previous",
333            "transaction_next",
334            "transaction_prev",
335            "copy_value",
336            "frame_buffer_set_array",
337            "frame_buffer_set_variable",
338            "frame_buffer_set_mode",
339            "frame_buffer_set_width",
340            "frame_buffer_set_range",
341            "pause_simulation",
342            "unpause_simulation",
343            "undo",
344            "redo",
345            #[cfg(not(target_arch = "wasm32"))]
346            wcp_start_or_stop,
347            #[cfg(not(target_arch = "wasm32"))]
348            "exit",
349        ]
350    } else {
351        vec![
352            "load_file",
353            "load_url",
354            #[cfg(not(target_arch = "wasm32"))]
355            "load_state",
356            "run_command_file",
357            "run_command_file_from_url",
358            "config_reload",
359            "theme_select",
360            "toggle_menu",
361            "toggle_side_panel",
362            "toggle_fullscreen",
363            "preference_set_clock_highlight",
364            "preference_set_hierarchy_style",
365            "preference_set_arrow_key_bindings",
366            "show_controls",
367            "show_mouse_gestures",
368            "show_quick_start",
369            "show_logs",
370            #[cfg(not(target_arch = "wasm32"))]
371            "create_default_config",
372            #[cfg(feature = "performance_plot")]
373            "show_performance",
374            #[cfg(not(target_arch = "wasm32"))]
375            wcp_start_or_stop,
376            #[cfg(not(target_arch = "wasm32"))]
377            "exit",
378        ]
379    };
380    if !surver_file_names.is_empty() {
381        commands.push("surver_select_file");
382        commands.push("surver_switch_file");
383    }
384
385    let mut theme_names = state.user.config.theme.theme_names.clone();
386    let state_file = state.user.state_file.clone();
387    let show_hierarchy = state.show_hierarchy();
388    let show_menu = state.show_menu();
389    let show_tick_lines = state.show_ticks();
390    theme_names.insert(0, "default".to_string());
391    Command::NonTerminal(
392        ParamGreed::Word,
393        commands.into_iter().map(std::convert::Into::into).collect(),
394        Box::new(move |query, _| {
395            let variables_in_active_scope = variables_in_active_scope.clone();
396            let markers = markers.clone();
397            let scopes = scopes.clone();
398            let active_scope = active_scope.clone();
399            let is_transaction_container = is_transaction_container;
400            match query {
401                "load_file" => single_word_delayed_suggestions(
402                    Box::new(all_wave_files),
403                    Box::new(|word| {
404                        Some(Command::Terminal(Message::LoadFile(
405                            word.into(),
406                            LoadOptions::Clear,
407                        )))
408                    }),
409                ),
410
411                "create_default_config" => Some(Command::Terminal(Message::DownloadDefaultConfig)),
412                "switch_file" => single_word_delayed_suggestions(
413                    Box::new(all_wave_files),
414                    Box::new(|word| {
415                        Some(Command::Terminal(Message::LoadFile(
416                            word.into(),
417                            LoadOptions::KeepAll,
418                        )))
419                    }),
420                ),
421                "load_url" => Some(Command::NonTerminal(
422                    ParamGreed::Rest,
423                    vec![],
424                    Box::new(|query, _| {
425                        Some(Command::Terminal(Message::LoadWaveformFileFromUrl(
426                            query.to_string(),
427                            LoadOptions::Clear, // load_url does not indicate any format restrictions
428                        )))
429                    }),
430                )),
431                "run_command_file" => single_word_delayed_suggestions(
432                    Box::new(all_command_files),
433                    Box::new(|word| Some(Command::Terminal(Message::LoadCommandFile(word.into())))),
434                ),
435                "run_command_file_from_url" => Some(Command::NonTerminal(
436                    ParamGreed::Rest,
437                    vec![],
438                    Box::new(|query, _| {
439                        Some(Command::Terminal(Message::LoadCommandFileFromUrl(
440                            query.to_string(),
441                        )))
442                    }),
443                )),
444                "config_reload" => Some(Command::Terminal(Message::ReloadConfig)),
445                "theme_select" => single_word(
446                    theme_names.clone(),
447                    Box::new(|word| {
448                        Some(Command::Terminal(Message::SelectTheme(Some(
449                            word.to_owned(),
450                        ))))
451                    }),
452                ),
453                "scroll_to_start" | "goto_start" => {
454                    Some(Command::Terminal(Message::GoToStart { viewport_idx: 0 }))
455                }
456                "scroll_to_end" | "goto_end" => {
457                    Some(Command::Terminal(Message::GoToEnd { viewport_idx: 0 }))
458                }
459                "zoom_in" => Some(Command::Terminal(Message::CanvasZoom {
460                    mouse_ptr: None,
461                    delta: 0.5,
462                    viewport_idx: 0,
463                })),
464                "zoom_out" => Some(Command::Terminal(Message::CanvasZoom {
465                    mouse_ptr: None,
466                    delta: 2.0,
467                    viewport_idx: 0,
468                })),
469                "zoom_fit" => Some(Command::Terminal(Message::ZoomToFit { viewport_idx: 0 })),
470                "zoom_to" => {
471                    let timescale_for_zoom = timescale.clone();
472                    Some(Command::NonTerminal(
473                        ParamGreed::Rest,
474                        vec![],
475                        Box::new(move |params, _| {
476                            let parts: Vec<&str> = params.split_whitespace().collect();
477                            if parts.len() < 2 {
478                                return None;
479                            }
480
481                            // Reconstruct time strings, handling optional spaces between number and unit
482                            let mut time_strings = Vec::new();
483                            let mut i = 0;
484                            while i < parts.len() && time_strings.len() < 2 {
485                                let part = parts[i];
486                                // Check if this part is numeric (potentially followed by a unit in the next part)
487                                if part
488                                    .chars()
489                                    .next()
490                                    .is_some_and(|c| c.is_numeric() || c == '-')
491                                {
492                                    let time_str = if i + 1 < parts.len()
493                                        && !parts[i + 1].chars().next().unwrap_or('0').is_numeric()
494                                    {
495                                        // Next part looks like a unit, combine them
496                                        let combined = format!("{}{}", part, parts[i + 1]);
497                                        i += 2;
498                                        combined
499                                    } else {
500                                        // No unit following, use as-is
501                                        i += 1;
502                                        part.to_string()
503                                    };
504                                    time_strings.push(time_str);
505                                } else {
506                                    i += 1;
507                                }
508                            }
509
510                            if time_strings.len() < 2 {
511                                return None;
512                            }
513
514                            let start_time = if let Some(ts) = &timescale_for_zoom {
515                                crate::time::parse_time_string_to_ticks(&time_strings[0], ts)?
516                            } else {
517                                time_strings[0].parse().ok()?
518                            };
519
520                            let end_time = if let Some(ts) = &timescale_for_zoom {
521                                crate::time::parse_time_string_to_ticks(&time_strings[1], ts)?
522                            } else {
523                                time_strings[1].parse().ok()?
524                            };
525
526                            Some(Command::Terminal(Message::ZoomToRange {
527                                start: start_time,
528                                end: end_time,
529                                viewport_idx: 0,
530                            }))
531                        }),
532                    ))
533                }
534                "toggle_menu" => Some(Command::Terminal(Message::SetMenuVisible(!show_menu))),
535                "toggle_side_panel" => Some(Command::Terminal(Message::SetSidePanelVisible(
536                    !show_hierarchy,
537                ))),
538                "toggle_fullscreen" => Some(Command::Terminal(Message::ToggleFullscreen)),
539                "toggle_tick_lines" => {
540                    Some(Command::Terminal(Message::SetTickLines(!show_tick_lines)))
541                }
542                // scope commands
543                "scope_add" | "module_add" | "stream_add" | "scope_add_recursive" => {
544                    let recursive = query == "scope_add_recursive";
545                    if is_transaction_container {
546                        if recursive {
547                            warn!("Cannot recursively add transaction containers");
548                        }
549                        single_word(
550                            scopes,
551                            Box::new(|word| {
552                                Some(Command::Terminal(Message::AddAllFromStreamScope(
553                                    word.to_string(),
554                                )))
555                            }),
556                        )
557                    } else {
558                        single_word(
559                            scopes,
560                            Box::new(move |word| {
561                                Some(Command::Terminal(Message::AddScope(
562                                    ScopeRef::from_hierarchy_string(word),
563                                    recursive,
564                                )))
565                            }),
566                        )
567                    }
568                }
569                "scope_add_as_group" | "scope_add_as_group_recursive" => {
570                    let recursive = query == "scope_add_as_group_recursive";
571                    if is_transaction_container {
572                        warn!("Cannot add transaction containers as group");
573                        None
574                    } else {
575                        single_word(
576                            scopes,
577                            Box::new(move |word| {
578                                Some(Command::Terminal(Message::AddScopeAsGroup(
579                                    ScopeRef::from_hierarchy_string(word),
580                                    recursive,
581                                )))
582                            }),
583                        )
584                    }
585                }
586                "scope_select" | "stream_select" => {
587                    if is_transaction_container {
588                        single_word(
589                            scopes.clone(),
590                            Box::new(|word| {
591                                let scope = if word == "tr" {
592                                    ScopeType::StreamScope(StreamScopeRef::Root)
593                                } else {
594                                    ScopeType::StreamScope(StreamScopeRef::Empty(word.to_string()))
595                                };
596                                Some(Command::Terminal(Message::SetActiveScope(Some(scope))))
597                            }),
598                        )
599                    } else {
600                        single_word(
601                            scopes.clone(),
602                            Box::new(|word| {
603                                Some(Command::Terminal(Message::SetActiveScope(Some(
604                                    ScopeType::WaveScope(ScopeRef::from_hierarchy_string(word)),
605                                ))))
606                            }),
607                        )
608                    }
609                }
610                "scope_select_root" | "stream_select_root" => {
611                    Some(Command::Terminal(Message::SetActiveScope(None)))
612                }
613                "reload" => Some(Command::Terminal(Message::ReloadWaveform(
614                    keep_during_reload,
615                ))),
616                "remove_unavailable" => Some(Command::Terminal(Message::RemovePlaceholders)),
617                "surver_select_file" => single_word(
618                    surver_file_names.clone(),
619                    Box::new(|word| {
620                        Some(Command::Terminal(Message::LoadSurverFileByName(
621                            word.to_string(),
622                            LoadOptions::Clear,
623                        )))
624                    }),
625                ),
626                "surver_switch_file" => single_word(
627                    surver_file_names.clone(),
628                    Box::new(|word| {
629                        Some(Command::Terminal(Message::LoadSurverFileByName(
630                            word.to_string(),
631                            LoadOptions::KeepAll,
632                        )))
633                    }),
634                ),
635                // Variable commands
636                "variable_add" | "generator_add" => {
637                    if is_transaction_container {
638                        single_word(
639                            variables.clone(),
640                            Box::new(|word| {
641                                Some(Command::Terminal(Message::AddStreamOrGeneratorFromName(
642                                    None,
643                                    word.to_string(),
644                                )))
645                            }),
646                        )
647                    } else {
648                        single_word(
649                            variables.clone(),
650                            Box::new(|word| {
651                                Some(Command::Terminal(Message::AddVariables(vec![
652                                    VariableRef::from_hierarchy_string(word),
653                                ])))
654                            }),
655                        )
656                    }
657                }
658                "variable_add_from_scope" | "generator_add_from_stream" => single_word(
659                    variables_in_active_scope
660                        .into_iter()
661                        .map(|s| s.name_with_index())
662                        .collect(),
663                    Box::new(move |name| {
664                        active_scope.as_ref().map(|scope| match scope {
665                            ScopeType::WaveScope(w) => Command::Terminal(Message::AddVariables(
666                                vec![VariableRef::new(w.clone(), name.to_string())],
667                            )),
668                            ScopeType::StreamScope(stream_scope) => {
669                                Command::Terminal(Message::AddStreamOrGeneratorFromName(
670                                    Some(stream_scope.clone()),
671                                    name.to_string(),
672                                ))
673                            }
674                        })
675                    }),
676                ),
677                "item_set_color" => single_word(
678                    color_names.clone(),
679                    Box::new(|word| {
680                        Some(Command::Terminal(Message::ItemColorChange(
681                            MessageTarget::CurrentSelection,
682                            Some(word.to_string()),
683                        )))
684                    }),
685                ),
686                "item_set_background_color" => single_word(
687                    color_names.clone(),
688                    Box::new(|word| {
689                        Some(Command::Terminal(Message::ItemBackgroundColorChange(
690                            MessageTarget::CurrentSelection,
691                            Some(word.to_string()),
692                        )))
693                    }),
694                ),
695                "item_unset_color" => Some(Command::Terminal(Message::ItemColorChange(
696                    MessageTarget::CurrentSelection,
697                    None,
698                ))),
699                "item_set_format" => single_word(
700                    format_names.clone(),
701                    Box::new(|word| {
702                        Some(Command::Terminal(Message::VariableFormatChange(
703                            MessageTarget::CurrentSelection,
704                            word.to_string(),
705                        )))
706                    }),
707                ),
708                "item_unset_background_color" => Some(Command::Terminal(
709                    Message::ItemBackgroundColorChange(MessageTarget::CurrentSelection, None),
710                )),
711                "item_rename" => Some(Command::NonTerminal(
712                    ParamGreed::Rest,
713                    vec![],
714                    Box::new(|query, _| {
715                        Some(Command::Terminal(Message::ItemNameChange(
716                            None,
717                            Some(query.to_owned()),
718                        )))
719                    }),
720                )),
721                "variable_set_name_type" => single_word(
722                    vec![
723                        "Local".to_string(),
724                        "Unique".to_string(),
725                        "Global".to_string(),
726                    ],
727                    Box::new(|word| {
728                        Some(Command::Terminal(Message::ChangeVariableNameType(
729                            MessageTarget::CurrentSelection,
730                            VariableNameType::from_str(word).unwrap_or(VariableNameType::Local),
731                        )))
732                    }),
733                ),
734                "variable_force_name_type" => single_word(
735                    vec![
736                        "Local".to_string(),
737                        "Unique".to_string(),
738                        "Global".to_string(),
739                    ],
740                    Box::new(|word| {
741                        Some(Command::Terminal(Message::ForceVariableNameTypes(
742                            VariableNameType::from_str(word).unwrap_or(VariableNameType::Local),
743                        )))
744                    }),
745                ),
746                "item_focus" => single_word(
747                    displayed_items.clone(),
748                    Box::new(|word| {
749                        // split off the idx which is always followed by an underscore
750                        let alpha_idx: String = word.chars().take_while(|c| *c != '_').collect();
751                        alpha_idx_to_uint_idx(&alpha_idx)
752                            .map(|idx| Command::Terminal(Message::FocusItem(idx)))
753                    }),
754                ),
755                "transition_next" => single_word(
756                    displayed_items.clone(),
757                    Box::new(|word| {
758                        // split off the idx which is always followed by an underscore
759                        let alpha_idx: String = word.chars().take_while(|c| *c != '_').collect();
760                        alpha_idx_to_uint_idx(&alpha_idx).map(|idx| {
761                            Command::Terminal(Message::MoveCursorToTransition {
762                                next: true,
763                                variable: Some(idx),
764                                skip_zero: false,
765                            })
766                        })
767                    }),
768                ),
769                "transition_previous" => single_word(
770                    displayed_items.clone(),
771                    Box::new(|word| {
772                        // split off the idx which is always followed by an underscore
773                        let alpha_idx: String = word.chars().take_while(|c| *c != '_').collect();
774                        alpha_idx_to_uint_idx(&alpha_idx).map(|idx| {
775                            Command::Terminal(Message::MoveCursorToTransition {
776                                next: false,
777                                variable: Some(idx),
778                                skip_zero: false,
779                            })
780                        })
781                    }),
782                ),
783                "transaction_next" => {
784                    Some(Command::Terminal(Message::MoveTransaction { next: true }))
785                }
786                "transaction_prev" => {
787                    Some(Command::Terminal(Message::MoveTransaction { next: false }))
788                }
789                "copy_value" => single_word(
790                    displayed_items.clone(),
791                    Box::new(|word| {
792                        // split off the idx which is always followed by an underscore
793                        let alpha_idx: String = word.chars().take_while(|c| *c != '_').collect();
794                        alpha_idx_to_uint_idx(&alpha_idx).map(|idx| {
795                            Command::Terminal(Message::VariableValueToClipbord(
796                                MessageTarget::Explicit(idx),
797                            ))
798                        })
799                    }),
800                ),
801                "preference_set_clock_highlight" => single_word(
802                    ["Line", "Cycle", "None"]
803                        .iter()
804                        .map(ToString::to_string)
805                        .collect_vec(),
806                    Box::new(|word| {
807                        Some(Command::Terminal(Message::SetClockHighlightType(
808                            ClockHighlightType::from_str(word).unwrap_or(ClockHighlightType::Line),
809                        )))
810                    }),
811                ),
812                "preference_set_hierarchy_style" => single_word(
813                    enum_iterator::all::<HierarchyStyle>()
814                        .map(|o| o.to_string())
815                        .collect_vec(),
816                    Box::new(|word| {
817                        Some(Command::Terminal(Message::SetHierarchyStyle(
818                            HierarchyStyle::from_str(word).unwrap_or(HierarchyStyle::Separate),
819                        )))
820                    }),
821                ),
822                "preference_set_arrow_key_bindings" => single_word(
823                    enum_iterator::all::<ArrowKeyBindings>()
824                        .map(|o| o.to_string())
825                        .collect_vec(),
826                    Box::new(|word| {
827                        Some(Command::Terminal(Message::SetArrowKeyBindings(
828                            ArrowKeyBindings::from_str(word).unwrap_or(ArrowKeyBindings::Edge),
829                        )))
830                    }),
831                ),
832                "item_unfocus" => Some(Command::Terminal(Message::UnfocusItem)),
833                "divider_add" => optional_single_word(
834                    vec![],
835                    Box::new(|word| {
836                        Some(Command::Terminal(Message::AddDivider(
837                            Some(word.into()),
838                            None,
839                        )))
840                    }),
841                ),
842                "timeline_add" => Some(Command::Terminal(Message::AddTimeLine(None))),
843                "goto_cursor" => Some(Command::Terminal(Message::GoToCursorIfNotInView)),
844                "goto_marker" => single_word(
845                    marker_suggestions(&markers),
846                    Box::new(move |name| {
847                        parse_marker(name, &markers)
848                            .map(|idx| Command::Terminal(Message::GoToMarkerPosition(idx, 0)))
849                    }),
850                ),
851                "frame_buffer_set_array" => single_word(
852                    arrays.clone(),
853                    Box::new(|word| {
854                        Some(Command::Terminal(Message::SetFrameBufferArray(
855                            ScopeRef::from_hierarchy_string(word),
856                        )))
857                    }),
858                ),
859                "frame_buffer_set_variable" => single_word(
860                    variables.clone(),
861                    Box::new(|word| {
862                        Some(Command::Terminal(Message::SetFrameBufferVariable(
863                            VariableRef::from_hierarchy_string(word),
864                        )))
865                    }),
866                ),
867                "frame_buffer_set_mode" => Some(Command::NonTerminal(
868                    ParamGreed::Word,
869                    vec![
870                        "grayscale".to_string(),
871                        "rgb".to_string(),
872                        "ycbcr".to_string(),
873                    ],
874                    Box::new(|word, _| {
875                        let mode = match word {
876                            "grayscale" => FrameBufferColorMode::Grayscale,
877                            "rgb" => FrameBufferColorMode::Rgb,
878                            "ycbcr" => FrameBufferColorMode::YCbCr,
879                            _ => return None,
880                        };
881                        Some(Command::NonTerminal(
882                            ParamGreed::Rest,
883                            vec![],
884                            Box::new(move |rest, _| {
885                                let args: Vec<&str> = rest.split_whitespace().collect();
886                                match mode {
887                                    FrameBufferColorMode::Grayscale => {
888                                        if args.len() != 1 {
889                                            return None;
890                                        }
891                                        let bits = args[0].parse::<u8>().ok()?;
892                                        if !(1..=8).contains(&bits) {
893                                            return None;
894                                        }
895                                        Some(Command::Terminal(Message::SetFrameBufferMode(
896                                            mode, bits, 0, 0,
897                                        )))
898                                    }
899                                    FrameBufferColorMode::Rgb | FrameBufferColorMode::YCbCr => {
900                                        if args.len() != 3 {
901                                            return None;
902                                        }
903                                        let bits1 = args[0].parse::<u8>().ok()?;
904                                        let bits2 = args[1].parse::<u8>().ok()?;
905                                        let bits3 = args[2].parse::<u8>().ok()?;
906                                        if bits1 > 8 || bits2 > 8 || bits3 > 8 {
907                                            return None;
908                                        }
909                                        Some(Command::Terminal(Message::SetFrameBufferMode(
910                                            mode, bits1, bits2, bits3,
911                                        )))
912                                    }
913                                }
914                            }),
915                        ))
916                    }),
917                )),
918                "frame_buffer_set_width" => single_word(
919                    vec![],
920                    Box::new(|word| {
921                        let width = word.parse::<usize>().ok()?.max(1);
922                        Some(Command::Terminal(Message::SetFrameBufferWidth(width)))
923                    }),
924                ),
925                "frame_buffer_set_range" => single_word(
926                    vec![],
927                    Box::new(|rest| {
928                        let values: Vec<i64> = rest
929                            .split_whitespace()
930                            .map(str::parse::<i64>)
931                            .collect::<Result<_, _>>()
932                            .ok()?;
933                        if values.is_empty() || !values.len().is_multiple_of(2) {
934                            return None;
935                        }
936
937                        let pairs = values
938                            .chunks_exact(2)
939                            .map(|c| (c[0], c[1]))
940                            .collect::<Vec<_>>();
941                        Some(Command::Terminal(Message::SetFrameBufferRange(pairs)))
942                    }),
943                ),
944                "dump_tree" => Some(Command::Terminal(Message::DumpTree)),
945                "group_marked" => optional_single_word(
946                    vec![],
947                    Box::new(|name| {
948                        let trimmed = name.trim();
949                        Some(Command::Terminal(Message::GroupNew {
950                            name: (!trimmed.is_empty()).then_some(trimmed.to_owned()),
951                            before: None,
952                            items: None,
953                        }))
954                    }),
955                ),
956                "group_dissolve" => Some(Command::Terminal(Message::GroupDissolve(None))),
957                "group_fold_recursive" => {
958                    Some(Command::Terminal(Message::GroupFoldRecursive(None)))
959                }
960                "group_unfold_recursive" => {
961                    Some(Command::Terminal(Message::GroupUnfoldRecursive(None)))
962                }
963                "group_fold_all" => Some(Command::Terminal(Message::GroupFoldAll)),
964                "group_unfold_all" => Some(Command::Terminal(Message::GroupUnfoldAll)),
965                "show_controls" => Some(Command::Terminal(Message::SetKeyHelpVisible(true))),
966                "show_mouse_gestures" => {
967                    Some(Command::Terminal(Message::SetGestureHelpVisible(true)))
968                }
969                "show_quick_start" => Some(Command::Terminal(Message::SetQuickStartVisible(true))),
970                #[cfg(feature = "performance_plot")]
971                "show_performance" => optional_single_word(
972                    vec![],
973                    Box::new(|word| {
974                        if word == "redraw" {
975                            Some(Command::Terminal(Message::Batch(vec![
976                                Message::SetPerformanceVisible(true),
977                                Message::SetContinuousRedraw(true),
978                            ])))
979                        } else {
980                            Some(Command::Terminal(Message::SetPerformanceVisible(true)))
981                        }
982                    }),
983                ),
984                "cursor_set" => {
985                    let timescale_for_cursor = timescale.clone();
986                    single_word(
987                        vec![],
988                        Box::new(move |time_str| {
989                            let time = if let Some(ts) = &timescale_for_cursor {
990                                crate::time::parse_time_string_to_ticks(time_str, ts)?
991                            } else {
992                                time_str.parse().ok()?
993                            };
994                            Some(Command::Terminal(Message::Batch(vec![
995                                Message::CursorSet(time),
996                                Message::GoToCursorIfNotInView,
997                            ])))
998                        }),
999                    )
1000                }
1001                "goto_time" => {
1002                    let timescale_for_goto = timescale.clone();
1003                    single_word(
1004                        vec![],
1005                        Box::new(move |time_str| {
1006                            let time = if let Some(ts) = &timescale_for_goto {
1007                                crate::time::parse_time_string_to_ticks(time_str, ts)?
1008                            } else {
1009                                time_str.parse().ok()?
1010                            };
1011                            Some(Command::Terminal(Message::GoToTime(Some(time), 0)))
1012                        }),
1013                    )
1014                }
1015                "marker_set" => Some(Command::NonTerminal(
1016                    ParamGreed::Custom(&separate_at_space),
1017                    // FIXME use once fzcmd does not enforce suggestion match, as of now we couldn't add a marker (except the first)
1018                    // marker_suggestions(&markers),
1019                    vec![],
1020                    Box::new(move |name, _| {
1021                        let marker_id = parse_marker(name, &markers);
1022                        let name = name.to_owned();
1023
1024                        Some(Command::NonTerminal(
1025                            ParamGreed::Word,
1026                            vec![],
1027                            Box::new(move |time_str, _| {
1028                                let time = time_str.parse().ok()?;
1029                                match marker_id {
1030                                    Some(id) => {
1031                                        Some(Command::Terminal(Message::SetMarker { id, time }))
1032                                    }
1033                                    None => Some(Command::Terminal(Message::AddMarker {
1034                                        time,
1035                                        name: Some(name.clone()),
1036                                        move_focus: true,
1037                                    })),
1038                                }
1039                            }),
1040                        ))
1041                    }),
1042                )),
1043                "marker_set_at" => {
1044                    let timescale_for_marker_set_at = timescale.clone();
1045                    Some(Command::NonTerminal(
1046                        ParamGreed::Rest,
1047                        vec![],
1048                        Box::new(move |query, _| {
1049                            let parts = query.split_whitespace().collect_vec();
1050                            if parts.len() < 2 {
1051                                return None;
1052                            }
1053
1054                            let (time_str, marker_ref) = if parts.len() >= 3 {
1055                                let combined = format!("{}{}", parts[0], parts[1]);
1056                                if let Some(ts) = &timescale_for_marker_set_at {
1057                                    if crate::time::parse_time_string_to_ticks(&combined, ts)
1058                                        .is_some()
1059                                    {
1060                                        (combined, parts[2..].join(" "))
1061                                    } else {
1062                                        (parts[0].to_string(), parts[1..].join(" "))
1063                                    }
1064                                } else {
1065                                    (parts[0].to_string(), parts[1..].join(" "))
1066                                }
1067                            } else {
1068                                (parts[0].to_string(), parts[1].to_string())
1069                            };
1070
1071                            let time = if let Some(ts) = &timescale_for_marker_set_at {
1072                                crate::time::parse_time_string_to_ticks(&time_str, ts)?
1073                            } else {
1074                                time_str.parse().ok()?
1075                            };
1076
1077                            let marker_id = parse_marker(&marker_ref, &markers);
1078                            match marker_id {
1079                                Some(id) => {
1080                                    Some(Command::Terminal(Message::SetMarker { id, time }))
1081                                }
1082                                None => Some(Command::Terminal(Message::AddMarker {
1083                                    time,
1084                                    name: Some(marker_ref),
1085                                    move_focus: true,
1086                                })),
1087                            }
1088                        }),
1089                    ))
1090                }
1091                "marker_remove" => Some(Command::NonTerminal(
1092                    ParamGreed::Rest,
1093                    marker_suggestions(&markers),
1094                    Box::new(move |name, _| {
1095                        let marker_id = parse_marker(name, &markers)?;
1096                        Some(Command::Terminal(Message::RemoveMarker(marker_id)))
1097                    }),
1098                )),
1099                "show_marker_window" => {
1100                    Some(Command::Terminal(Message::SetCursorWindowVisible(true)))
1101                }
1102                "show_logs" => Some(Command::Terminal(Message::SetLogsVisible(true))),
1103                "save_state" => Some(Command::Terminal(Message::SaveStateFile(
1104                    state_file.clone(),
1105                ))),
1106                "save_state_as" => single_word(
1107                    vec![],
1108                    Box::new(|word| {
1109                        Some(Command::Terminal(Message::SaveStateFile(Some(
1110                            std::path::Path::new(word).into(),
1111                        ))))
1112                    }),
1113                ),
1114                "load_state" => single_word(
1115                    vec![],
1116                    Box::new(|word| {
1117                        Some(Command::Terminal(Message::LoadStateFile(Some(
1118                            std::path::Path::new(word).into(),
1119                        ))))
1120                    }),
1121                ),
1122                "viewport_add" => Some(Command::Terminal(Message::AddViewport)),
1123                "viewport_remove" => Some(Command::Terminal(Message::RemoveViewport)),
1124                "pause_simulation" => Some(Command::Terminal(Message::PauseSimulation)),
1125                "unpause_simulation" => Some(Command::Terminal(Message::UnpauseSimulation)),
1126                "undo" => Some(Command::Terminal(Message::Undo(1))),
1127                "redo" => Some(Command::Terminal(Message::Redo(1))),
1128                "wcp_server_start" => Some(Command::Terminal(Message::StartWcpServer {
1129                    address: None,
1130                    initiate: false,
1131                })),
1132                "wcp_server_stop" => Some(Command::Terminal(Message::StopWcpServer)),
1133                "exit" => Some(Command::Terminal(Message::Exit)),
1134                _ => None,
1135            }
1136        }),
1137    )
1138}