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