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