1use crate::{
7 reporter::{MaxProgressRunning, ShowProgress},
8 user_config::helpers::resolve_ui_setting,
9};
10use serde::{
11 Deserialize, Deserializer,
12 de::{self, Unexpected},
13};
14use std::{collections::BTreeMap, fmt, process::Command};
15use target_spec::{Platform, TargetSpec};
16
17#[derive(Clone, Debug, Default, Deserialize)]
22#[serde(rename_all = "kebab-case")]
23pub(in crate::user_config) struct DeserializedUiConfig {
24 pub(in crate::user_config) show_progress: Option<UiShowProgress>,
28
29 #[serde(default, deserialize_with = "deserialize_max_progress_running")]
33 max_progress_running: Option<MaxProgressRunning>,
34
35 input_handler: Option<bool>,
37
38 output_indent: Option<bool>,
40
41 #[serde(default)]
43 pager: Option<PagerSetting>,
44
45 #[serde(default)]
47 paginate: Option<PaginateSetting>,
48
49 #[serde(default)]
51 streampager: DeserializedStreampagerConfig,
52}
53
54#[derive(Clone, Debug, Deserialize)]
59#[serde(rename_all = "kebab-case")]
60pub(crate) struct DefaultUiConfig {
61 show_progress: UiShowProgress,
63
64 #[serde(deserialize_with = "deserialize_max_progress_running_required")]
66 max_progress_running: MaxProgressRunning,
67
68 input_handler: bool,
70
71 output_indent: bool,
73
74 pub(in crate::user_config) pager: PagerSetting,
76
77 pub(in crate::user_config) paginate: PaginateSetting,
79
80 pub(in crate::user_config) streampager: DefaultStreampagerConfig,
82}
83
84#[derive(Clone, Debug, Default, Deserialize)]
89#[serde(rename_all = "kebab-case")]
90pub(in crate::user_config) struct DeserializedUiOverrideData {
91 pub(in crate::user_config) show_progress: Option<UiShowProgress>,
93
94 #[serde(default, deserialize_with = "deserialize_max_progress_running")]
96 pub(in crate::user_config) max_progress_running: Option<MaxProgressRunning>,
97
98 pub(in crate::user_config) input_handler: Option<bool>,
100
101 pub(in crate::user_config) output_indent: Option<bool>,
103
104 #[serde(default)]
106 pub(in crate::user_config) pager: Option<PagerSetting>,
107
108 #[serde(default)]
110 pub(in crate::user_config) paginate: Option<PaginateSetting>,
111
112 #[serde(default)]
114 pub(in crate::user_config) streampager: DeserializedStreampagerConfig,
115}
116
117#[derive(Clone, Debug)]
122pub(in crate::user_config) struct CompiledUiOverride {
123 platform_spec: TargetSpec,
124 data: UiOverrideData,
125}
126
127impl CompiledUiOverride {
128 pub(in crate::user_config) fn new(
130 platform_spec: TargetSpec,
131 data: DeserializedUiOverrideData,
132 ) -> Self {
133 Self {
134 platform_spec,
135 data: UiOverrideData {
136 show_progress: data.show_progress,
137 max_progress_running: data.max_progress_running,
138 input_handler: data.input_handler,
139 output_indent: data.output_indent,
140 pager: data.pager,
141 paginate: data.paginate,
142 streampager_interface: data.streampager.interface,
143 streampager_wrapping: data.streampager.wrapping,
144 streampager_show_ruler: data.streampager.show_ruler,
145 },
146 }
147 }
148
149 pub(in crate::user_config) fn matches(&self, host_platform: &Platform) -> bool {
154 self.platform_spec
155 .eval(host_platform)
156 .unwrap_or(false)
157 }
158
159 pub(in crate::user_config) fn data(&self) -> &UiOverrideData {
161 &self.data
162 }
163}
164
165#[derive(Clone, Debug, Default)]
167pub(in crate::user_config) struct UiOverrideData {
168 show_progress: Option<UiShowProgress>,
169 max_progress_running: Option<MaxProgressRunning>,
170 input_handler: Option<bool>,
171 output_indent: Option<bool>,
172 pager: Option<PagerSetting>,
173 paginate: Option<PaginateSetting>,
174 streampager_interface: Option<StreampagerInterface>,
175 streampager_wrapping: Option<StreampagerWrapping>,
176 streampager_show_ruler: Option<bool>,
177}
178
179impl UiOverrideData {
180 pub(in crate::user_config) fn pager(&self) -> Option<&PagerSetting> {
182 self.pager.as_ref()
183 }
184
185 pub(in crate::user_config) fn paginate(&self) -> Option<&PaginateSetting> {
187 self.paginate.as_ref()
188 }
189
190 pub(in crate::user_config) fn streampager_interface(&self) -> Option<&StreampagerInterface> {
192 self.streampager_interface.as_ref()
193 }
194
195 pub(in crate::user_config) fn streampager_wrapping(&self) -> Option<&StreampagerWrapping> {
197 self.streampager_wrapping.as_ref()
198 }
199
200 pub(in crate::user_config) fn streampager_show_ruler(&self) -> Option<&bool> {
202 self.streampager_show_ruler.as_ref()
203 }
204}
205
206#[derive(Clone, Debug)]
211pub struct UiConfig {
212 pub show_progress: UiShowProgress,
214 pub max_progress_running: MaxProgressRunning,
216 pub input_handler: bool,
218 pub output_indent: bool,
220 pub pager: PagerSetting,
222 pub paginate: PaginateSetting,
224 pub streampager: StreampagerConfig,
226}
227
228impl UiConfig {
229 pub(in crate::user_config) fn resolve(
241 default_config: &DefaultUiConfig,
242 default_overrides: &[CompiledUiOverride],
243 user_config: Option<&DeserializedUiConfig>,
244 user_overrides: &[CompiledUiOverride],
245 host_platform: &Platform,
246 ) -> Self {
247 Self {
248 show_progress: resolve_ui_setting(
249 &default_config.show_progress,
250 default_overrides,
251 user_config.and_then(|c| c.show_progress.as_ref()),
252 user_overrides,
253 host_platform,
254 |data| data.show_progress.as_ref(),
255 ),
256 max_progress_running: resolve_ui_setting(
257 &default_config.max_progress_running,
258 default_overrides,
259 user_config.and_then(|c| c.max_progress_running.as_ref()),
260 user_overrides,
261 host_platform,
262 |data| data.max_progress_running.as_ref(),
263 ),
264 input_handler: resolve_ui_setting(
265 &default_config.input_handler,
266 default_overrides,
267 user_config.and_then(|c| c.input_handler.as_ref()),
268 user_overrides,
269 host_platform,
270 |data| data.input_handler.as_ref(),
271 ),
272 output_indent: resolve_ui_setting(
273 &default_config.output_indent,
274 default_overrides,
275 user_config.and_then(|c| c.output_indent.as_ref()),
276 user_overrides,
277 host_platform,
278 |data| data.output_indent.as_ref(),
279 ),
280 pager: resolve_ui_setting(
281 &default_config.pager,
282 default_overrides,
283 user_config.and_then(|c| c.pager.as_ref()),
284 user_overrides,
285 host_platform,
286 |data| data.pager.as_ref(),
287 ),
288 paginate: resolve_ui_setting(
289 &default_config.paginate,
290 default_overrides,
291 user_config.and_then(|c| c.paginate.as_ref()),
292 user_overrides,
293 host_platform,
294 |data| data.paginate.as_ref(),
295 ),
296 streampager: StreampagerConfig {
297 interface: resolve_ui_setting(
298 &default_config.streampager.interface,
299 default_overrides,
300 user_config.and_then(|c| c.streampager.interface.as_ref()),
301 user_overrides,
302 host_platform,
303 |data| data.streampager_interface.as_ref(),
304 ),
305 wrapping: resolve_ui_setting(
306 &default_config.streampager.wrapping,
307 default_overrides,
308 user_config.and_then(|c| c.streampager.wrapping.as_ref()),
309 user_overrides,
310 host_platform,
311 |data| data.streampager_wrapping.as_ref(),
312 ),
313 show_ruler: resolve_ui_setting(
314 &default_config.streampager.show_ruler,
315 default_overrides,
316 user_config.and_then(|c| c.streampager.show_ruler.as_ref()),
317 user_overrides,
318 host_platform,
319 |data| data.streampager_show_ruler.as_ref(),
320 ),
321 },
322 }
323 }
324}
325
326#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
332#[serde(rename_all = "kebab-case")]
333pub enum UiShowProgress {
334 #[default]
336 Auto,
337 None,
339 Bar,
341 Counter,
343 Only,
349}
350
351impl From<UiShowProgress> for ShowProgress {
352 fn from(ui: UiShowProgress) -> Self {
353 match ui {
354 UiShowProgress::Auto => ShowProgress::Auto {
355 suppress_success: false,
356 },
357 UiShowProgress::None => ShowProgress::None,
358 UiShowProgress::Bar => ShowProgress::Running,
359 UiShowProgress::Counter => ShowProgress::Counter,
360 UiShowProgress::Only => ShowProgress::Auto {
361 suppress_success: true,
362 },
363 }
364 }
365}
366
367#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
369#[serde(rename_all = "kebab-case")]
370pub enum PaginateSetting {
371 #[default]
373 Auto,
374 Never,
376}
377
378pub const BUILTIN_PAGER_NAME: &str = ":builtin";
380
381#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
385#[serde(rename_all = "kebab-case")]
386pub(in crate::user_config) struct DeserializedStreampagerConfig {
387 pub(in crate::user_config) interface: Option<StreampagerInterface>,
389 pub(in crate::user_config) wrapping: Option<StreampagerWrapping>,
391 pub(in crate::user_config) show_ruler: Option<bool>,
393}
394
395#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
399#[serde(rename_all = "kebab-case")]
400pub(in crate::user_config) struct DefaultStreampagerConfig {
401 pub(in crate::user_config) interface: StreampagerInterface,
403 pub(in crate::user_config) wrapping: StreampagerWrapping,
405 pub(in crate::user_config) show_ruler: bool,
407}
408
409#[derive(Clone, Copy, Debug, PartialEq, Eq)]
413pub struct StreampagerConfig {
414 pub interface: StreampagerInterface,
416 pub wrapping: StreampagerWrapping,
418 pub show_ruler: bool,
420}
421
422impl StreampagerConfig {
423 pub fn streampager_interface_mode(&self) -> streampager::config::InterfaceMode {
425 use streampager::config::InterfaceMode;
426 match self.interface {
427 StreampagerInterface::FullScreenClearOutput => InterfaceMode::FullScreen,
428 StreampagerInterface::QuitIfOnePage => InterfaceMode::Hybrid,
429 StreampagerInterface::QuitQuicklyOrClearOutput => {
430 InterfaceMode::Delayed(std::time::Duration::from_secs(2))
431 }
432 }
433 }
434
435 pub fn streampager_wrapping_mode(&self) -> streampager::config::WrappingMode {
437 use streampager::config::WrappingMode;
438 match self.wrapping {
439 StreampagerWrapping::None => WrappingMode::Unwrapped,
440 StreampagerWrapping::Word => WrappingMode::WordBoundary,
441 StreampagerWrapping::Anywhere => WrappingMode::GraphemeBoundary,
442 }
443 }
444}
445
446#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
450#[serde(rename_all = "kebab-case")]
451pub enum StreampagerInterface {
452 #[default]
455 QuitIfOnePage,
456 FullScreenClearOutput,
458 QuitQuicklyOrClearOutput,
460}
461
462#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
464#[serde(rename_all = "kebab-case")]
465pub enum StreampagerWrapping {
466 None,
468 #[default]
470 Word,
471 Anywhere,
473}
474
475#[derive(Clone, Debug, PartialEq, Eq)]
483pub struct CommandNameAndArgs {
484 command: Vec<String>,
486 env: BTreeMap<String, String>,
488}
489
490impl CommandNameAndArgs {
491 pub fn command_name(&self) -> &str {
493 &self.command[0]
495 }
496
497 pub fn args(&self) -> &[String] {
499 &self.command[1..]
500 }
501
502 pub fn to_command(&self) -> Command {
504 let mut cmd = Command::new(self.command_name());
505 cmd.args(self.args());
506 cmd.envs(&self.env);
507 cmd
508 }
509}
510
511impl<'de> Deserialize<'de> for CommandNameAndArgs {
512 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
513 deserializer.deserialize_any(CommandNameAndArgsVisitor)
514 }
515}
516
517struct CommandNameAndArgsVisitor;
519
520impl<'de> de::Visitor<'de> for CommandNameAndArgsVisitor {
521 type Value = CommandNameAndArgs;
522
523 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
524 formatter.write_str(
525 "a command string (\"less -FRX\"), \
526 an array ([\"less\", \"-FRX\"]), \
527 or a table ({ command = [\"less\", \"-FRX\"], env = { ... } })",
528 )
529 }
530
531 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
532 let command: Vec<String> = shell_words::split(v).map_err(de::Error::custom)?;
533 if command.is_empty() {
534 return Err(de::Error::custom("command string must not be empty"));
535 }
536 Ok(CommandNameAndArgs {
537 command,
538 env: BTreeMap::new(),
539 })
540 }
541
542 fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
543 let mut command = Vec::new();
544 while let Some(arg) = seq.next_element::<String>()? {
545 command.push(arg);
546 }
547 if command.is_empty() {
548 return Err(de::Error::custom("command array must not be empty"));
549 }
550 Ok(CommandNameAndArgs {
551 command,
552 env: BTreeMap::new(),
553 })
554 }
555
556 fn visit_map<A: de::MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
557 #[derive(Deserialize)]
558 struct StructuredInner {
559 command: Vec<String>,
560 #[serde(default)]
561 env: BTreeMap<String, String>,
562 }
563
564 let inner = StructuredInner::deserialize(de::value::MapAccessDeserializer::new(map))?;
565 if inner.command.is_empty() {
566 return Err(de::Error::custom("command array must not be empty"));
567 }
568 Ok(CommandNameAndArgs {
569 command: inner.command,
570 env: inner.env,
571 })
572 }
573}
574
575#[derive(Clone, Debug, PartialEq, Eq)]
580pub enum PagerSetting {
581 Builtin,
583 External(CommandNameAndArgs),
585}
586
587#[cfg(test)]
590impl Default for PagerSetting {
591 fn default() -> Self {
592 Self::External(CommandNameAndArgs {
593 command: vec!["less".to_owned(), "-FRX".to_owned()],
594 env: [("LESSCHARSET".to_owned(), "utf-8".to_owned())]
595 .into_iter()
596 .collect(),
597 })
598 }
599}
600
601impl<'de> Deserialize<'de> for PagerSetting {
602 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
603 deserializer.deserialize_any(PagerSettingVisitor)
604 }
605}
606
607struct PagerSettingVisitor;
609
610impl<'de> de::Visitor<'de> for PagerSettingVisitor {
611 type Value = PagerSetting;
612
613 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
614 formatter
615 .write_str("\":builtin\", a command string, an array, or a table with command and env")
616 }
617
618 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
619 if v == BUILTIN_PAGER_NAME {
621 return Ok(PagerSetting::Builtin);
622 }
623 let cmd = CommandNameAndArgsVisitor.visit_str(v)?;
624 Ok(PagerSetting::External(cmd))
625 }
626
627 fn visit_seq<A: de::SeqAccess<'de>>(self, seq: A) -> Result<Self::Value, A::Error> {
628 let args = CommandNameAndArgsVisitor.visit_seq(seq)?;
629 Ok(PagerSetting::External(args))
630 }
631
632 fn visit_map<A: de::MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
633 let args = CommandNameAndArgsVisitor.visit_map(map)?;
634 Ok(PagerSetting::External(args))
635 }
636}
637
638struct MaxProgressRunningVisitor;
640
641impl<'de> de::Visitor<'de> for MaxProgressRunningVisitor {
642 type Value = MaxProgressRunning;
643
644 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
645 formatter.write_str("a non-negative integer or \"infinite\"")
646 }
647
648 fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
649 Ok(MaxProgressRunning::Count(v as usize))
650 }
651
652 fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
653 if v < 0 {
654 Err(E::invalid_value(Unexpected::Signed(v), &self))
655 } else {
656 Ok(MaxProgressRunning::Count(v as usize))
657 }
658 }
659
660 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
661 if v == "infinite" {
662 Ok(MaxProgressRunning::Infinite)
663 } else {
664 v.parse::<usize>()
666 .map(MaxProgressRunning::Count)
667 .map_err(|_| E::invalid_value(Unexpected::Str(v), &self))
668 }
669 }
670}
671
672fn deserialize_max_progress_running<'de, D>(
673 deserializer: D,
674) -> Result<Option<MaxProgressRunning>, D::Error>
675where
676 D: Deserializer<'de>,
677{
678 deserializer.deserialize_option(OptionMaxProgressRunningVisitor)
679}
680
681struct OptionMaxProgressRunningVisitor;
683
684impl<'de> de::Visitor<'de> for OptionMaxProgressRunningVisitor {
685 type Value = Option<MaxProgressRunning>;
686
687 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
688 formatter.write_str("a non-negative integer, \"infinite\", or null")
689 }
690
691 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
692 Ok(None)
693 }
694
695 fn visit_some<D: Deserializer<'de>>(self, deserializer: D) -> Result<Self::Value, D::Error> {
696 deserializer
697 .deserialize_any(MaxProgressRunningVisitor)
698 .map(Some)
699 }
700
701 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
702 Ok(None)
703 }
704}
705
706fn deserialize_max_progress_running_required<'de, D>(
707 deserializer: D,
708) -> Result<MaxProgressRunning, D::Error>
709where
710 D: Deserializer<'de>,
711{
712 deserializer.deserialize_any(MaxProgressRunningVisitor)
713}
714
715#[cfg(test)]
716mod tests {
717 use super::*;
718 use crate::{platform::detect_host_platform_for_tests, user_config::DefaultUserConfig};
719
720 fn make_override(platform: &str, data: DeserializedUiOverrideData) -> CompiledUiOverride {
722 let platform_spec =
723 TargetSpec::new(platform.to_string()).expect("valid platform spec in test");
724 CompiledUiOverride::new(platform_spec, data)
725 }
726
727 #[test]
728 fn test_ui_config_show_progress() {
729 let config: DeserializedUiConfig = toml::from_str(r#"show-progress = "auto""#).unwrap();
731 assert!(matches!(config.show_progress, Some(UiShowProgress::Auto)));
732
733 let config: DeserializedUiConfig = toml::from_str(r#"show-progress = "none""#).unwrap();
734 assert!(matches!(config.show_progress, Some(UiShowProgress::None)));
735
736 let config: DeserializedUiConfig = toml::from_str(r#"show-progress = "bar""#).unwrap();
737 assert!(matches!(config.show_progress, Some(UiShowProgress::Bar)));
738
739 let config: DeserializedUiConfig = toml::from_str(r#"show-progress = "counter""#).unwrap();
740 assert!(matches!(
741 config.show_progress,
742 Some(UiShowProgress::Counter)
743 ));
744
745 let config: DeserializedUiConfig = toml::from_str(r#"show-progress = "only""#).unwrap();
746 assert!(matches!(config.show_progress, Some(UiShowProgress::Only)));
747
748 let config: DeserializedUiConfig = toml::from_str("").unwrap();
750 assert!(config.show_progress.is_none());
751
752 toml::from_str::<DeserializedUiConfig>(r#"show-progress = "invalid""#).unwrap_err();
754 }
755
756 #[test]
757 fn test_ui_show_progress_to_show_progress() {
758 assert_eq!(
760 ShowProgress::from(UiShowProgress::Auto),
761 ShowProgress::Auto {
762 suppress_success: false
763 }
764 );
765 assert_eq!(ShowProgress::from(UiShowProgress::None), ShowProgress::None);
766 assert_eq!(
767 ShowProgress::from(UiShowProgress::Bar),
768 ShowProgress::Running
769 );
770 assert_eq!(
771 ShowProgress::from(UiShowProgress::Counter),
772 ShowProgress::Counter
773 );
774 assert_eq!(
777 ShowProgress::from(UiShowProgress::Only),
778 ShowProgress::Auto {
779 suppress_success: true
780 }
781 );
782 }
783
784 #[test]
785 fn test_ui_config_max_progress_running() {
786 let config: DeserializedUiConfig = toml::from_str("max-progress-running = 10").unwrap();
788 assert!(matches!(
789 config.max_progress_running,
790 Some(MaxProgressRunning::Count(10))
791 ));
792
793 let config: DeserializedUiConfig = toml::from_str("max-progress-running = 0").unwrap();
794 assert!(matches!(
795 config.max_progress_running,
796 Some(MaxProgressRunning::Count(0))
797 ));
798
799 let config: DeserializedUiConfig =
801 toml::from_str(r#"max-progress-running = "infinite""#).unwrap();
802 assert!(matches!(
803 config.max_progress_running,
804 Some(MaxProgressRunning::Infinite)
805 ));
806
807 toml::from_str::<DeserializedUiConfig>(r#"max-progress-running = "INFINITE""#).unwrap_err();
809
810 let config: DeserializedUiConfig = toml::from_str("").unwrap();
812 assert!(config.max_progress_running.is_none());
813
814 toml::from_str::<DeserializedUiConfig>(r#"max-progress-running = "invalid""#).unwrap_err();
816 }
817
818 #[test]
819 fn test_ui_config_input_handler() {
820 let config: DeserializedUiConfig = toml::from_str("input-handler = true").unwrap();
821 assert_eq!(config.input_handler, Some(true));
822 let config: DeserializedUiConfig = toml::from_str("input-handler = false").unwrap();
823 assert_eq!(config.input_handler, Some(false));
824 let config: DeserializedUiConfig = toml::from_str("").unwrap();
825 assert!(config.input_handler.is_none());
826 }
827
828 #[test]
829 fn test_ui_config_output_indent() {
830 let config: DeserializedUiConfig = toml::from_str("output-indent = true").unwrap();
831 assert_eq!(config.output_indent, Some(true));
832 let config: DeserializedUiConfig = toml::from_str("output-indent = false").unwrap();
833 assert_eq!(config.output_indent, Some(false));
834 let config: DeserializedUiConfig = toml::from_str("").unwrap();
835 assert!(config.output_indent.is_none());
836 }
837
838 #[test]
839 fn test_resolved_ui_config_defaults_only() {
840 let defaults = DefaultUserConfig::from_embedded().ui;
841
842 let host = detect_host_platform_for_tests();
843 let resolved = UiConfig::resolve(&defaults, &[], None, &[], &host);
844
845 assert_eq!(resolved.show_progress, defaults.show_progress);
847 assert_eq!(resolved.max_progress_running, defaults.max_progress_running);
848 assert_eq!(resolved.input_handler, defaults.input_handler);
849 assert_eq!(resolved.output_indent, defaults.output_indent);
850 }
851
852 #[test]
853 fn test_resolved_ui_config_user_config_overrides_defaults() {
854 let defaults = DefaultUserConfig::from_embedded().ui;
855
856 let user_config = DeserializedUiConfig {
857 show_progress: Some(UiShowProgress::Bar),
858 max_progress_running: Some(MaxProgressRunning::Count(4)),
859 output_indent: Some(false),
860 ..Default::default()
861 };
862
863 let host = detect_host_platform_for_tests();
864 let resolved = UiConfig::resolve(&defaults, &[], Some(&user_config), &[], &host);
865
866 assert_eq!(resolved.show_progress, UiShowProgress::Bar);
867 assert_eq!(resolved.max_progress_running, MaxProgressRunning::Count(4));
868 assert_eq!(resolved.input_handler, defaults.input_handler); assert!(!resolved.output_indent);
870 }
871
872 #[test]
873 fn test_resolved_ui_config_user_override_applies() {
874 let defaults = DefaultUserConfig::from_embedded().ui;
875
876 let override_ = make_override(
878 "cfg(all())",
879 DeserializedUiOverrideData {
880 show_progress: Some(UiShowProgress::Counter),
881 input_handler: Some(false),
882 ..Default::default()
883 },
884 );
885
886 let host = detect_host_platform_for_tests();
887 let resolved = UiConfig::resolve(&defaults, &[], None, &[override_], &host);
888
889 assert_eq!(resolved.show_progress, UiShowProgress::Counter);
890 assert_eq!(resolved.max_progress_running, defaults.max_progress_running); assert!(!resolved.input_handler);
892 assert_eq!(resolved.output_indent, defaults.output_indent); }
894
895 #[test]
896 fn test_resolved_ui_config_default_override_applies() {
897 let defaults = DefaultUserConfig::from_embedded().ui;
898
899 let override_ = make_override(
901 "cfg(all())",
902 DeserializedUiOverrideData {
903 show_progress: Some(UiShowProgress::Counter),
904 input_handler: Some(false),
905 ..Default::default()
906 },
907 );
908
909 let host = detect_host_platform_for_tests();
910 let resolved = UiConfig::resolve(&defaults, &[override_], None, &[], &host);
911
912 assert_eq!(resolved.show_progress, UiShowProgress::Counter);
913 assert_eq!(resolved.max_progress_running, defaults.max_progress_running); assert!(!resolved.input_handler);
915 assert_eq!(resolved.output_indent, defaults.output_indent); }
917
918 #[test]
919 fn test_resolved_ui_config_platform_override_no_match() {
920 let defaults = DefaultUserConfig::from_embedded().ui;
921
922 let override_ = make_override(
925 "cfg(any())",
926 DeserializedUiOverrideData {
927 show_progress: Some(UiShowProgress::Counter),
928 max_progress_running: Some(MaxProgressRunning::Count(2)),
929 input_handler: Some(false),
930 output_indent: Some(false),
931 pager: Some(PagerSetting::default()),
932 paginate: Some(PaginateSetting::Never),
933 streampager: Default::default(),
934 },
935 );
936
937 let host = detect_host_platform_for_tests();
938 let resolved = UiConfig::resolve(&defaults, &[], None, &[override_], &host);
939
940 assert_eq!(resolved.show_progress, defaults.show_progress);
942 assert_eq!(resolved.max_progress_running, defaults.max_progress_running);
943 assert_eq!(resolved.input_handler, defaults.input_handler);
944 assert_eq!(resolved.output_indent, defaults.output_indent);
945 }
946
947 #[test]
948 fn test_resolved_ui_config_first_matching_user_override_wins() {
949 let defaults = DefaultUserConfig::from_embedded().ui;
950
951 let override1 = make_override(
953 "cfg(all())",
954 DeserializedUiOverrideData {
955 show_progress: Some(UiShowProgress::Bar),
956 ..Default::default()
957 },
958 );
959
960 let override2 = make_override(
961 "cfg(all())",
962 DeserializedUiOverrideData {
963 show_progress: Some(UiShowProgress::Counter), max_progress_running: Some(MaxProgressRunning::Count(4)),
965 ..Default::default()
966 },
967 );
968
969 let host = detect_host_platform_for_tests();
970 let resolved = UiConfig::resolve(&defaults, &[], None, &[override1, override2], &host);
971
972 assert_eq!(resolved.show_progress, UiShowProgress::Bar);
974 assert_eq!(resolved.max_progress_running, MaxProgressRunning::Count(4));
976 }
977
978 #[test]
979 fn test_resolved_ui_config_user_override_beats_default_override() {
980 let defaults = DefaultUserConfig::from_embedded().ui;
981
982 let user_override = make_override(
984 "cfg(all())",
985 DeserializedUiOverrideData {
986 show_progress: Some(UiShowProgress::Bar),
987 ..Default::default()
988 },
989 );
990
991 let default_override = make_override(
993 "cfg(all())",
994 DeserializedUiOverrideData {
995 show_progress: Some(UiShowProgress::Counter), max_progress_running: Some(MaxProgressRunning::Count(4)),
997 ..Default::default()
998 },
999 );
1000
1001 let host = detect_host_platform_for_tests();
1002 let resolved = UiConfig::resolve(
1003 &defaults,
1004 &[default_override],
1005 None,
1006 &[user_override],
1007 &host,
1008 );
1009
1010 assert_eq!(resolved.show_progress, UiShowProgress::Bar);
1012 assert_eq!(resolved.max_progress_running, MaxProgressRunning::Count(4));
1014 }
1015
1016 #[test]
1017 fn test_resolved_ui_config_override_beats_user_base() {
1018 let defaults = DefaultUserConfig::from_embedded().ui;
1019
1020 let user_config = DeserializedUiConfig {
1022 show_progress: Some(UiShowProgress::None),
1023 max_progress_running: Some(MaxProgressRunning::Count(2)),
1024 ..Default::default()
1025 };
1026
1027 let default_override = make_override(
1029 "cfg(all())",
1030 DeserializedUiOverrideData {
1031 show_progress: Some(UiShowProgress::Counter),
1032 ..Default::default()
1033 },
1034 );
1035
1036 let host = detect_host_platform_for_tests();
1037 let resolved = UiConfig::resolve(
1038 &defaults,
1039 &[default_override],
1040 Some(&user_config),
1041 &[],
1042 &host,
1043 );
1044
1045 assert_eq!(resolved.show_progress, UiShowProgress::Counter);
1047 assert_eq!(resolved.max_progress_running, MaxProgressRunning::Count(2));
1049 }
1050
1051 #[test]
1052 fn test_paginate_setting_parsing() {
1053 let config: DeserializedUiConfig = toml::from_str(r#"paginate = "auto""#).unwrap();
1055 assert_eq!(config.paginate, Some(PaginateSetting::Auto));
1056
1057 let config: DeserializedUiConfig = toml::from_str(r#"paginate = "never""#).unwrap();
1059 assert_eq!(config.paginate, Some(PaginateSetting::Never));
1060
1061 let config: DeserializedUiConfig = toml::from_str("").unwrap();
1063 assert!(config.paginate.is_none());
1064
1065 let err = toml::from_str::<DeserializedUiConfig>(r#"paginate = "invalid""#).unwrap_err();
1067 assert!(
1068 err.to_string().contains("unknown variant"),
1069 "error should mention 'unknown variant': {err}"
1070 );
1071 }
1072
1073 #[test]
1074 fn test_command_name_and_args_parsing() {
1075 #[derive(Debug, Deserialize)]
1076 struct Wrapper {
1077 cmd: CommandNameAndArgs,
1078 }
1079
1080 let wrapper: Wrapper = toml::from_str(r#"cmd = "less -FRX""#).unwrap();
1082 assert_eq!(
1083 wrapper.cmd,
1084 CommandNameAndArgs {
1085 command: vec!["less".to_owned(), "-FRX".to_owned()],
1086 env: BTreeMap::new(),
1087 }
1088 );
1089 assert_eq!(wrapper.cmd.command_name(), "less");
1090 assert_eq!(wrapper.cmd.args(), &["-FRX".to_owned()]);
1091
1092 let wrapper: Wrapper = toml::from_str(r#"cmd = ["less", "-F", "-R", "-X"]"#).unwrap();
1094 assert_eq!(
1095 wrapper.cmd,
1096 CommandNameAndArgs {
1097 command: vec![
1098 "less".to_owned(),
1099 "-F".to_owned(),
1100 "-R".to_owned(),
1101 "-X".to_owned()
1102 ],
1103 env: BTreeMap::new(),
1104 }
1105 );
1106 assert_eq!(wrapper.cmd.command_name(), "less");
1107 assert_eq!(
1108 wrapper.cmd.args(),
1109 &["-F".to_owned(), "-R".to_owned(), "-X".to_owned()]
1110 );
1111
1112 let cmd: CommandNameAndArgs = toml::from_str(
1114 r#"
1115 command = ["less", "-FRX"]
1116 env = { LESSCHARSET = "utf-8" }
1117 "#,
1118 )
1119 .unwrap();
1120 let expected_env: BTreeMap<String, String> =
1121 [("LESSCHARSET".to_owned(), "utf-8".to_owned())]
1122 .into_iter()
1123 .collect();
1124 assert_eq!(
1125 cmd,
1126 CommandNameAndArgs {
1127 command: vec!["less".to_owned(), "-FRX".to_owned()],
1128 env: expected_env,
1129 }
1130 );
1131 assert_eq!(cmd.command_name(), "less");
1132 assert_eq!(cmd.args(), &["-FRX".to_owned()]);
1133
1134 let wrapper: Wrapper = toml::from_str(r#"cmd = 'my-pager "arg with spaces"'"#).unwrap();
1136 assert_eq!(
1137 wrapper.cmd,
1138 CommandNameAndArgs {
1139 command: vec!["my-pager".to_owned(), "arg with spaces".to_owned()],
1140 env: BTreeMap::new(),
1141 }
1142 );
1143
1144 let wrapper: Wrapper = toml::from_str(r#"cmd = "my-pager 'arg with spaces'""#).unwrap();
1146 assert_eq!(
1147 wrapper.cmd,
1148 CommandNameAndArgs {
1149 command: vec!["my-pager".to_owned(), "arg with spaces".to_owned()],
1150 env: BTreeMap::new(),
1151 }
1152 );
1153
1154 let wrapper: Wrapper =
1156 toml::from_str(r#"cmd = 'my-pager "quoted \"nested\" arg"'"#).unwrap();
1157 assert_eq!(
1158 wrapper.cmd,
1159 CommandNameAndArgs {
1160 command: vec!["my-pager".to_owned(), "quoted \"nested\" arg".to_owned()],
1161 env: BTreeMap::new(),
1162 }
1163 );
1164
1165 let wrapper: Wrapper = toml::from_str(r#"cmd = '"/path/to/my pager" --flag'"#).unwrap();
1167 assert_eq!(
1168 wrapper.cmd,
1169 CommandNameAndArgs {
1170 command: vec!["/path/to/my pager".to_owned(), "--flag".to_owned()],
1171 env: BTreeMap::new(),
1172 }
1173 );
1174
1175 let wrapper: Wrapper =
1177 toml::from_str(r#"cmd = 'cmd "first arg" "second arg" third'"#).unwrap();
1178 assert_eq!(
1179 wrapper.cmd,
1180 CommandNameAndArgs {
1181 command: vec![
1182 "cmd".to_owned(),
1183 "first arg".to_owned(),
1184 "second arg".to_owned(),
1185 "third".to_owned(),
1186 ],
1187 env: BTreeMap::new(),
1188 }
1189 );
1190 }
1191
1192 #[test]
1193 fn test_command_and_pager_empty_errors() {
1194 #[derive(Debug, Deserialize)]
1195 struct Wrapper {
1196 #[expect(dead_code)]
1197 cmd: CommandNameAndArgs,
1198 }
1199
1200 let cmd_cases = [
1202 ("empty array", "cmd = []"),
1203 ("empty string", r#"cmd = """#),
1204 ("whitespace-only string", r#"cmd = " ""#),
1205 (
1206 "structured with empty command",
1207 r#"cmd = { command = [], env = { LESSCHARSET = "utf-8" } }"#,
1208 ),
1209 ];
1210
1211 for (name, input) in cmd_cases {
1212 let err = toml::from_str::<Wrapper>(input).unwrap_err();
1213 assert!(
1214 err.to_string().contains("must not be empty"),
1215 "CommandNameAndArgs {name}: error should mention 'must not be empty': {err}"
1216 );
1217 }
1218
1219 let pager_cases = [
1221 ("empty array", "pager = []"),
1222 ("empty string", r#"pager = """#),
1223 ];
1224
1225 for (name, input) in pager_cases {
1226 let err = toml::from_str::<DeserializedUiConfig>(input).unwrap_err();
1227 assert!(
1228 err.to_string().contains("must not be empty"),
1229 "PagerSetting {name}: error should mention 'must not be empty': {err}"
1230 );
1231 }
1232
1233 let unclosed_quote_cases = [
1235 ("unclosed double quote", r#"cmd = 'pager "unclosed'"#),
1236 ("unclosed single quote", r#"cmd = "pager 'unclosed""#),
1237 ];
1238
1239 for (name, input) in unclosed_quote_cases {
1240 let err = toml::from_str::<Wrapper>(input).unwrap_err();
1241 assert!(
1242 err.to_string().contains("missing closing quote"),
1243 "CommandNameAndArgs {name}: error should mention 'missing closing quote': {err}"
1244 );
1245 }
1246 }
1247
1248 #[test]
1249 fn test_command_name_and_args_to_command() {
1250 let cmd = CommandNameAndArgs {
1252 command: vec!["echo".to_owned(), "hello".to_owned()],
1253 env: BTreeMap::new(),
1254 };
1255 let std_cmd = cmd.to_command();
1256 assert_eq!(cmd.command_name(), "echo");
1257 drop(std_cmd);
1258 }
1259
1260 #[test]
1261 fn test_pager_setting_parsing() {
1262 let config: DeserializedUiConfig = toml::from_str(r#"pager = "less -FRX""#).unwrap();
1264 assert_eq!(
1265 config.pager,
1266 Some(PagerSetting::External(CommandNameAndArgs {
1267 command: vec!["less".to_owned(), "-FRX".to_owned()],
1268 env: BTreeMap::new(),
1269 }))
1270 );
1271
1272 let config: DeserializedUiConfig = toml::from_str(r#"pager = ["less", "-FRX"]"#).unwrap();
1274 assert_eq!(
1275 config.pager,
1276 Some(PagerSetting::External(CommandNameAndArgs {
1277 command: vec!["less".to_owned(), "-FRX".to_owned()],
1278 env: BTreeMap::new(),
1279 }))
1280 );
1281
1282 let config: DeserializedUiConfig = toml::from_str(
1284 r#"
1285 [pager]
1286 command = ["less", "-FRX"]
1287 env = { LESSCHARSET = "utf-8" }
1288 "#,
1289 )
1290 .unwrap();
1291 let expected_env: BTreeMap<String, String> =
1292 [("LESSCHARSET".to_owned(), "utf-8".to_owned())]
1293 .into_iter()
1294 .collect();
1295 assert_eq!(
1296 config.pager,
1297 Some(PagerSetting::External(CommandNameAndArgs {
1298 command: vec!["less".to_owned(), "-FRX".to_owned()],
1299 env: expected_env,
1300 }))
1301 );
1302
1303 let config: DeserializedUiConfig = toml::from_str("").unwrap();
1305 assert!(config.pager.is_none());
1306 }
1307
1308 #[test]
1309 fn test_resolved_ui_config_pager_defaults() {
1310 let defaults = DefaultUserConfig::from_embedded().ui;
1311
1312 let host = detect_host_platform_for_tests();
1313 let resolved = UiConfig::resolve(&defaults, &[], None, &[], &host);
1314
1315 assert_eq!(resolved.pager, defaults.pager);
1317 assert_eq!(resolved.paginate, defaults.paginate);
1318 }
1319
1320 #[test]
1321 fn test_resolved_ui_config_pager_override() {
1322 let defaults = DefaultUserConfig::from_embedded().ui;
1323
1324 let custom_pager = PagerSetting::External(CommandNameAndArgs {
1326 command: vec!["more".to_owned()],
1327 env: BTreeMap::new(),
1328 });
1329 let override_ = make_override(
1330 "cfg(all())",
1331 DeserializedUiOverrideData {
1332 pager: Some(custom_pager.clone()),
1333 ..Default::default()
1334 },
1335 );
1336
1337 let host = detect_host_platform_for_tests();
1338 let resolved = UiConfig::resolve(&defaults, &[], None, &[override_], &host);
1339
1340 assert_eq!(resolved.pager, custom_pager);
1341 assert_eq!(resolved.paginate, defaults.paginate);
1343 }
1344
1345 #[test]
1346 fn test_resolved_ui_config_paginate_override() {
1347 let defaults = DefaultUserConfig::from_embedded().ui;
1348
1349 let override_ = make_override(
1351 "cfg(all())",
1352 DeserializedUiOverrideData {
1353 paginate: Some(PaginateSetting::Never),
1354 ..Default::default()
1355 },
1356 );
1357
1358 let host = detect_host_platform_for_tests();
1359 let resolved = UiConfig::resolve(&defaults, &[], None, &[override_], &host);
1360
1361 assert_eq!(resolved.paginate, PaginateSetting::Never);
1362 assert_eq!(resolved.pager, defaults.pager);
1364 }
1365
1366 #[test]
1367 fn test_pager_setting_builtin() {
1368 let config: DeserializedUiConfig = toml::from_str(r#"pager = ":builtin""#).unwrap();
1370 assert_eq!(config.pager, Some(PagerSetting::Builtin));
1371 }
1372
1373 #[test]
1374 fn test_streampager_config_parsing() {
1375 let config: DeserializedUiConfig = toml::from_str(
1377 r#"
1378 [streampager]
1379 interface = "full-screen-clear-output"
1380 wrapping = "anywhere"
1381 show-ruler = false
1382 "#,
1383 )
1384 .unwrap();
1385 assert_eq!(
1386 config.streampager.interface,
1387 Some(StreampagerInterface::FullScreenClearOutput)
1388 );
1389 assert_eq!(
1390 config.streampager.wrapping,
1391 Some(StreampagerWrapping::Anywhere)
1392 );
1393 assert_eq!(config.streampager.show_ruler, Some(false));
1394
1395 let config: DeserializedUiConfig = toml::from_str(
1397 r#"
1398 [streampager]
1399 interface = "quit-quickly-or-clear-output"
1400 "#,
1401 )
1402 .unwrap();
1403 assert_eq!(
1404 config.streampager.interface,
1405 Some(StreampagerInterface::QuitQuicklyOrClearOutput)
1406 );
1407 assert_eq!(config.streampager.wrapping, None);
1408 assert_eq!(config.streampager.show_ruler, None);
1409
1410 let config: DeserializedUiConfig = toml::from_str("").unwrap();
1412 assert_eq!(config.streampager.interface, None);
1413 assert_eq!(config.streampager.wrapping, None);
1414 assert_eq!(config.streampager.show_ruler, None);
1415 }
1416
1417 #[test]
1418 fn test_streampager_config_resolution() {
1419 let defaults = DefaultUserConfig::from_embedded().ui;
1420
1421 let override_ = make_override(
1423 "cfg(all())",
1424 DeserializedUiOverrideData {
1425 streampager: DeserializedStreampagerConfig {
1426 interface: Some(StreampagerInterface::FullScreenClearOutput),
1427 wrapping: None,
1428 show_ruler: None,
1429 },
1430 ..Default::default()
1431 },
1432 );
1433
1434 let host = detect_host_platform_for_tests();
1435 let resolved = UiConfig::resolve(&defaults, &[], None, &[override_], &host);
1436
1437 assert_eq!(
1439 resolved.streampager.interface,
1440 StreampagerInterface::FullScreenClearOutput
1441 );
1442 assert_eq!(resolved.streampager.wrapping, defaults.streampager.wrapping);
1444 assert_eq!(
1445 resolved.streampager.show_ruler,
1446 defaults.streampager.show_ruler
1447 );
1448 }
1449}