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