1use 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
30fn is_wave_file_extension(ext: &str) -> bool {
32 matches!(ext, "vcd" | "fst" | "ghw")
33}
34
35fn is_command_file_extension(ext: &str) -> bool {
37 matches!(ext, "sucl")
38}
39
40fn 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, )))
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 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 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 let combined = format!("{}{}", part, parts[i + 1]);
528 i += 2;
529 combined
530 } else {
531 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) = ×cale_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) = ×cale_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_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_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 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 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 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 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) = ×cale_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) = ×cale_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 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) = ×cale_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) = ×cale_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}