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