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)]
333#[serde(rename_all = "kebab-case")]
334pub enum UiShowProgress {
335 #[default]
337 Auto,
338 None,
340 Bar,
342 Counter,
344 Only,
347}
348
349impl From<UiShowProgress> for ShowProgress {
350 fn from(ui: UiShowProgress) -> Self {
351 match ui {
352 UiShowProgress::Auto => ShowProgress::Auto,
353 UiShowProgress::None => ShowProgress::None,
354 UiShowProgress::Bar | UiShowProgress::Only => ShowProgress::Running,
355 UiShowProgress::Counter => ShowProgress::Counter,
356 }
357 }
358}
359
360#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
362#[serde(rename_all = "kebab-case")]
363pub enum PaginateSetting {
364 #[default]
366 Auto,
367 Never,
369}
370
371pub const BUILTIN_PAGER_NAME: &str = ":builtin";
373
374#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
378#[serde(rename_all = "kebab-case")]
379pub(in crate::user_config) struct DeserializedStreampagerConfig {
380 pub(in crate::user_config) interface: Option<StreampagerInterface>,
382 pub(in crate::user_config) wrapping: Option<StreampagerWrapping>,
384 pub(in crate::user_config) show_ruler: Option<bool>,
386}
387
388#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
392#[serde(rename_all = "kebab-case")]
393pub(in crate::user_config) struct DefaultStreampagerConfig {
394 pub(in crate::user_config) interface: StreampagerInterface,
396 pub(in crate::user_config) wrapping: StreampagerWrapping,
398 pub(in crate::user_config) show_ruler: bool,
400}
401
402#[derive(Clone, Copy, Debug, PartialEq, Eq)]
406pub struct StreampagerConfig {
407 pub interface: StreampagerInterface,
409 pub wrapping: StreampagerWrapping,
411 pub show_ruler: bool,
413}
414
415impl StreampagerConfig {
416 pub fn streampager_interface_mode(&self) -> streampager::config::InterfaceMode {
418 use streampager::config::InterfaceMode;
419 match self.interface {
420 StreampagerInterface::FullScreenClearOutput => InterfaceMode::FullScreen,
421 StreampagerInterface::QuitIfOnePage => InterfaceMode::Hybrid,
422 StreampagerInterface::QuitQuicklyOrClearOutput => {
423 InterfaceMode::Delayed(std::time::Duration::from_secs(2))
424 }
425 }
426 }
427
428 pub fn streampager_wrapping_mode(&self) -> streampager::config::WrappingMode {
430 use streampager::config::WrappingMode;
431 match self.wrapping {
432 StreampagerWrapping::None => WrappingMode::Unwrapped,
433 StreampagerWrapping::Word => WrappingMode::WordBoundary,
434 StreampagerWrapping::Anywhere => WrappingMode::GraphemeBoundary,
435 }
436 }
437}
438
439#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
443#[serde(rename_all = "kebab-case")]
444pub enum StreampagerInterface {
445 #[default]
448 QuitIfOnePage,
449 FullScreenClearOutput,
451 QuitQuicklyOrClearOutput,
453}
454
455#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
457#[serde(rename_all = "kebab-case")]
458pub enum StreampagerWrapping {
459 None,
461 #[default]
463 Word,
464 Anywhere,
466}
467
468#[derive(Clone, Debug, PartialEq, Eq)]
476pub struct CommandNameAndArgs {
477 command: Vec<String>,
479 env: BTreeMap<String, String>,
481}
482
483impl CommandNameAndArgs {
484 pub fn command_name(&self) -> &str {
486 &self.command[0]
488 }
489
490 pub fn args(&self) -> &[String] {
492 &self.command[1..]
493 }
494
495 pub fn to_command(&self) -> Command {
497 let mut cmd = Command::new(self.command_name());
498 cmd.args(self.args());
499 cmd.envs(&self.env);
500 cmd
501 }
502}
503
504impl<'de> Deserialize<'de> for CommandNameAndArgs {
505 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
506 deserializer.deserialize_any(CommandNameAndArgsVisitor)
507 }
508}
509
510struct CommandNameAndArgsVisitor;
512
513impl<'de> de::Visitor<'de> for CommandNameAndArgsVisitor {
514 type Value = CommandNameAndArgs;
515
516 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
517 formatter.write_str(
518 "a command string (\"less -FRX\"), \
519 an array ([\"less\", \"-FRX\"]), \
520 or a table ({ command = [\"less\", \"-FRX\"], env = { ... } })",
521 )
522 }
523
524 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
525 let command: Vec<String> = shell_words::split(v).map_err(de::Error::custom)?;
526 if command.is_empty() {
527 return Err(de::Error::custom("command string must not be empty"));
528 }
529 Ok(CommandNameAndArgs {
530 command,
531 env: BTreeMap::new(),
532 })
533 }
534
535 fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
536 let mut command = Vec::new();
537 while let Some(arg) = seq.next_element::<String>()? {
538 command.push(arg);
539 }
540 if command.is_empty() {
541 return Err(de::Error::custom("command array must not be empty"));
542 }
543 Ok(CommandNameAndArgs {
544 command,
545 env: BTreeMap::new(),
546 })
547 }
548
549 fn visit_map<A: de::MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
550 #[derive(Deserialize)]
551 struct StructuredInner {
552 command: Vec<String>,
553 #[serde(default)]
554 env: BTreeMap<String, String>,
555 }
556
557 let inner = StructuredInner::deserialize(de::value::MapAccessDeserializer::new(map))?;
558 if inner.command.is_empty() {
559 return Err(de::Error::custom("command array must not be empty"));
560 }
561 Ok(CommandNameAndArgs {
562 command: inner.command,
563 env: inner.env,
564 })
565 }
566}
567
568#[derive(Clone, Debug, PartialEq, Eq)]
573pub enum PagerSetting {
574 Builtin,
576 External(CommandNameAndArgs),
578}
579
580#[cfg(test)]
583impl Default for PagerSetting {
584 fn default() -> Self {
585 Self::External(CommandNameAndArgs {
586 command: vec!["less".to_owned(), "-FRX".to_owned()],
587 env: [("LESSCHARSET".to_owned(), "utf-8".to_owned())]
588 .into_iter()
589 .collect(),
590 })
591 }
592}
593
594impl<'de> Deserialize<'de> for PagerSetting {
595 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
596 deserializer.deserialize_any(PagerSettingVisitor)
597 }
598}
599
600struct PagerSettingVisitor;
602
603impl<'de> de::Visitor<'de> for PagerSettingVisitor {
604 type Value = PagerSetting;
605
606 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
607 formatter
608 .write_str("\":builtin\", a command string, an array, or a table with command and env")
609 }
610
611 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
612 if v == BUILTIN_PAGER_NAME {
614 return Ok(PagerSetting::Builtin);
615 }
616 let cmd = CommandNameAndArgsVisitor.visit_str(v)?;
617 Ok(PagerSetting::External(cmd))
618 }
619
620 fn visit_seq<A: de::SeqAccess<'de>>(self, seq: A) -> Result<Self::Value, A::Error> {
621 let args = CommandNameAndArgsVisitor.visit_seq(seq)?;
622 Ok(PagerSetting::External(args))
623 }
624
625 fn visit_map<A: de::MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
626 let args = CommandNameAndArgsVisitor.visit_map(map)?;
627 Ok(PagerSetting::External(args))
628 }
629}
630
631struct MaxProgressRunningVisitor;
633
634impl<'de> de::Visitor<'de> for MaxProgressRunningVisitor {
635 type Value = MaxProgressRunning;
636
637 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
638 formatter.write_str("a non-negative integer or \"infinite\"")
639 }
640
641 fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
642 Ok(MaxProgressRunning::Count(v as usize))
643 }
644
645 fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
646 if v < 0 {
647 Err(E::invalid_value(Unexpected::Signed(v), &self))
648 } else {
649 Ok(MaxProgressRunning::Count(v as usize))
650 }
651 }
652
653 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
654 if v == "infinite" {
655 Ok(MaxProgressRunning::Infinite)
656 } else {
657 v.parse::<usize>()
659 .map(MaxProgressRunning::Count)
660 .map_err(|_| E::invalid_value(Unexpected::Str(v), &self))
661 }
662 }
663}
664
665fn deserialize_max_progress_running<'de, D>(
666 deserializer: D,
667) -> Result<Option<MaxProgressRunning>, D::Error>
668where
669 D: Deserializer<'de>,
670{
671 deserializer.deserialize_option(OptionMaxProgressRunningVisitor)
672}
673
674struct OptionMaxProgressRunningVisitor;
676
677impl<'de> de::Visitor<'de> for OptionMaxProgressRunningVisitor {
678 type Value = Option<MaxProgressRunning>;
679
680 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
681 formatter.write_str("a non-negative integer, \"infinite\", or null")
682 }
683
684 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
685 Ok(None)
686 }
687
688 fn visit_some<D: Deserializer<'de>>(self, deserializer: D) -> Result<Self::Value, D::Error> {
689 deserializer
690 .deserialize_any(MaxProgressRunningVisitor)
691 .map(Some)
692 }
693
694 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
695 Ok(None)
696 }
697}
698
699fn deserialize_max_progress_running_required<'de, D>(
700 deserializer: D,
701) -> Result<MaxProgressRunning, D::Error>
702where
703 D: Deserializer<'de>,
704{
705 deserializer.deserialize_any(MaxProgressRunningVisitor)
706}
707
708#[cfg(test)]
709mod tests {
710 use super::*;
711 use crate::{platform::detect_host_platform_for_tests, user_config::DefaultUserConfig};
712
713 fn make_override(platform: &str, data: DeserializedUiOverrideData) -> CompiledUiOverride {
715 let platform_spec =
716 TargetSpec::new(platform.to_string()).expect("valid platform spec in test");
717 CompiledUiOverride::new(platform_spec, data)
718 }
719
720 #[test]
721 fn test_ui_config_show_progress() {
722 let config: DeserializedUiConfig = toml::from_str(r#"show-progress = "auto""#).unwrap();
724 assert!(matches!(config.show_progress, Some(UiShowProgress::Auto)));
725
726 let config: DeserializedUiConfig = toml::from_str(r#"show-progress = "none""#).unwrap();
727 assert!(matches!(config.show_progress, Some(UiShowProgress::None)));
728
729 let config: DeserializedUiConfig = toml::from_str(r#"show-progress = "bar""#).unwrap();
730 assert!(matches!(config.show_progress, Some(UiShowProgress::Bar)));
731
732 let config: DeserializedUiConfig = toml::from_str(r#"show-progress = "counter""#).unwrap();
733 assert!(matches!(
734 config.show_progress,
735 Some(UiShowProgress::Counter)
736 ));
737
738 let config: DeserializedUiConfig = toml::from_str(r#"show-progress = "only""#).unwrap();
739 assert!(matches!(config.show_progress, Some(UiShowProgress::Only)));
740
741 let config: DeserializedUiConfig = toml::from_str("").unwrap();
743 assert!(config.show_progress.is_none());
744
745 toml::from_str::<DeserializedUiConfig>(r#"show-progress = "invalid""#).unwrap_err();
747 }
748
749 #[test]
750 fn test_ui_show_progress_to_show_progress() {
751 assert_eq!(ShowProgress::from(UiShowProgress::Auto), ShowProgress::Auto);
753 assert_eq!(ShowProgress::from(UiShowProgress::None), ShowProgress::None);
754 assert_eq!(
755 ShowProgress::from(UiShowProgress::Bar),
756 ShowProgress::Running
757 );
758 assert_eq!(
759 ShowProgress::from(UiShowProgress::Counter),
760 ShowProgress::Counter
761 );
762 assert_eq!(
764 ShowProgress::from(UiShowProgress::Only),
765 ShowProgress::Running
766 );
767 }
768
769 #[test]
770 fn test_ui_config_max_progress_running() {
771 let config: DeserializedUiConfig = toml::from_str("max-progress-running = 10").unwrap();
773 assert!(matches!(
774 config.max_progress_running,
775 Some(MaxProgressRunning::Count(10))
776 ));
777
778 let config: DeserializedUiConfig = toml::from_str("max-progress-running = 0").unwrap();
779 assert!(matches!(
780 config.max_progress_running,
781 Some(MaxProgressRunning::Count(0))
782 ));
783
784 let config: DeserializedUiConfig =
786 toml::from_str(r#"max-progress-running = "infinite""#).unwrap();
787 assert!(matches!(
788 config.max_progress_running,
789 Some(MaxProgressRunning::Infinite)
790 ));
791
792 toml::from_str::<DeserializedUiConfig>(r#"max-progress-running = "INFINITE""#).unwrap_err();
794
795 let config: DeserializedUiConfig = toml::from_str("").unwrap();
797 assert!(config.max_progress_running.is_none());
798
799 toml::from_str::<DeserializedUiConfig>(r#"max-progress-running = "invalid""#).unwrap_err();
801 }
802
803 #[test]
804 fn test_ui_config_input_handler() {
805 let config: DeserializedUiConfig = toml::from_str("input-handler = true").unwrap();
806 assert_eq!(config.input_handler, Some(true));
807 let config: DeserializedUiConfig = toml::from_str("input-handler = false").unwrap();
808 assert_eq!(config.input_handler, Some(false));
809 let config: DeserializedUiConfig = toml::from_str("").unwrap();
810 assert!(config.input_handler.is_none());
811 }
812
813 #[test]
814 fn test_ui_config_output_indent() {
815 let config: DeserializedUiConfig = toml::from_str("output-indent = true").unwrap();
816 assert_eq!(config.output_indent, Some(true));
817 let config: DeserializedUiConfig = toml::from_str("output-indent = false").unwrap();
818 assert_eq!(config.output_indent, Some(false));
819 let config: DeserializedUiConfig = toml::from_str("").unwrap();
820 assert!(config.output_indent.is_none());
821 }
822
823 #[test]
824 fn test_resolved_ui_config_defaults_only() {
825 let defaults = DefaultUserConfig::from_embedded().ui;
826
827 let host = detect_host_platform_for_tests();
828 let resolved = UiConfig::resolve(&defaults, &[], None, &[], &host);
829
830 assert_eq!(resolved.show_progress, defaults.show_progress);
832 assert_eq!(resolved.max_progress_running, defaults.max_progress_running);
833 assert_eq!(resolved.input_handler, defaults.input_handler);
834 assert_eq!(resolved.output_indent, defaults.output_indent);
835 }
836
837 #[test]
838 fn test_resolved_ui_config_user_config_overrides_defaults() {
839 let defaults = DefaultUserConfig::from_embedded().ui;
840
841 let user_config = DeserializedUiConfig {
842 show_progress: Some(UiShowProgress::Bar),
843 max_progress_running: Some(MaxProgressRunning::Count(4)),
844 output_indent: Some(false),
845 ..Default::default()
846 };
847
848 let host = detect_host_platform_for_tests();
849 let resolved = UiConfig::resolve(&defaults, &[], Some(&user_config), &[], &host);
850
851 assert_eq!(resolved.show_progress, UiShowProgress::Bar);
852 assert_eq!(resolved.max_progress_running, MaxProgressRunning::Count(4));
853 assert_eq!(resolved.input_handler, defaults.input_handler); assert!(!resolved.output_indent);
855 }
856
857 #[test]
858 fn test_resolved_ui_config_user_override_applies() {
859 let defaults = DefaultUserConfig::from_embedded().ui;
860
861 let override_ = make_override(
863 "cfg(all())",
864 DeserializedUiOverrideData {
865 show_progress: Some(UiShowProgress::Counter),
866 input_handler: Some(false),
867 ..Default::default()
868 },
869 );
870
871 let host = detect_host_platform_for_tests();
872 let resolved = UiConfig::resolve(&defaults, &[], None, &[override_], &host);
873
874 assert_eq!(resolved.show_progress, UiShowProgress::Counter);
875 assert_eq!(resolved.max_progress_running, defaults.max_progress_running); assert!(!resolved.input_handler);
877 assert_eq!(resolved.output_indent, defaults.output_indent); }
879
880 #[test]
881 fn test_resolved_ui_config_default_override_applies() {
882 let defaults = DefaultUserConfig::from_embedded().ui;
883
884 let override_ = make_override(
886 "cfg(all())",
887 DeserializedUiOverrideData {
888 show_progress: Some(UiShowProgress::Counter),
889 input_handler: Some(false),
890 ..Default::default()
891 },
892 );
893
894 let host = detect_host_platform_for_tests();
895 let resolved = UiConfig::resolve(&defaults, &[override_], None, &[], &host);
896
897 assert_eq!(resolved.show_progress, UiShowProgress::Counter);
898 assert_eq!(resolved.max_progress_running, defaults.max_progress_running); assert!(!resolved.input_handler);
900 assert_eq!(resolved.output_indent, defaults.output_indent); }
902
903 #[test]
904 fn test_resolved_ui_config_platform_override_no_match() {
905 let defaults = DefaultUserConfig::from_embedded().ui;
906
907 let override_ = make_override(
910 "cfg(any())",
911 DeserializedUiOverrideData {
912 show_progress: Some(UiShowProgress::Counter),
913 max_progress_running: Some(MaxProgressRunning::Count(2)),
914 input_handler: Some(false),
915 output_indent: Some(false),
916 pager: Some(PagerSetting::default()),
917 paginate: Some(PaginateSetting::Never),
918 streampager: Default::default(),
919 },
920 );
921
922 let host = detect_host_platform_for_tests();
923 let resolved = UiConfig::resolve(&defaults, &[], None, &[override_], &host);
924
925 assert_eq!(resolved.show_progress, defaults.show_progress);
927 assert_eq!(resolved.max_progress_running, defaults.max_progress_running);
928 assert_eq!(resolved.input_handler, defaults.input_handler);
929 assert_eq!(resolved.output_indent, defaults.output_indent);
930 }
931
932 #[test]
933 fn test_resolved_ui_config_first_matching_user_override_wins() {
934 let defaults = DefaultUserConfig::from_embedded().ui;
935
936 let override1 = make_override(
938 "cfg(all())",
939 DeserializedUiOverrideData {
940 show_progress: Some(UiShowProgress::Bar),
941 ..Default::default()
942 },
943 );
944
945 let override2 = make_override(
946 "cfg(all())",
947 DeserializedUiOverrideData {
948 show_progress: Some(UiShowProgress::Counter), max_progress_running: Some(MaxProgressRunning::Count(4)),
950 ..Default::default()
951 },
952 );
953
954 let host = detect_host_platform_for_tests();
955 let resolved = UiConfig::resolve(&defaults, &[], None, &[override1, override2], &host);
956
957 assert_eq!(resolved.show_progress, UiShowProgress::Bar);
959 assert_eq!(resolved.max_progress_running, MaxProgressRunning::Count(4));
961 }
962
963 #[test]
964 fn test_resolved_ui_config_user_override_beats_default_override() {
965 let defaults = DefaultUserConfig::from_embedded().ui;
966
967 let user_override = make_override(
969 "cfg(all())",
970 DeserializedUiOverrideData {
971 show_progress: Some(UiShowProgress::Bar),
972 ..Default::default()
973 },
974 );
975
976 let default_override = make_override(
978 "cfg(all())",
979 DeserializedUiOverrideData {
980 show_progress: Some(UiShowProgress::Counter), max_progress_running: Some(MaxProgressRunning::Count(4)),
982 ..Default::default()
983 },
984 );
985
986 let host = detect_host_platform_for_tests();
987 let resolved = UiConfig::resolve(
988 &defaults,
989 &[default_override],
990 None,
991 &[user_override],
992 &host,
993 );
994
995 assert_eq!(resolved.show_progress, UiShowProgress::Bar);
997 assert_eq!(resolved.max_progress_running, MaxProgressRunning::Count(4));
999 }
1000
1001 #[test]
1002 fn test_resolved_ui_config_override_beats_user_base() {
1003 let defaults = DefaultUserConfig::from_embedded().ui;
1004
1005 let user_config = DeserializedUiConfig {
1007 show_progress: Some(UiShowProgress::None),
1008 max_progress_running: Some(MaxProgressRunning::Count(2)),
1009 ..Default::default()
1010 };
1011
1012 let default_override = make_override(
1014 "cfg(all())",
1015 DeserializedUiOverrideData {
1016 show_progress: Some(UiShowProgress::Counter),
1017 ..Default::default()
1018 },
1019 );
1020
1021 let host = detect_host_platform_for_tests();
1022 let resolved = UiConfig::resolve(
1023 &defaults,
1024 &[default_override],
1025 Some(&user_config),
1026 &[],
1027 &host,
1028 );
1029
1030 assert_eq!(resolved.show_progress, UiShowProgress::Counter);
1032 assert_eq!(resolved.max_progress_running, MaxProgressRunning::Count(2));
1034 }
1035
1036 #[test]
1037 fn test_paginate_setting_parsing() {
1038 let config: DeserializedUiConfig = toml::from_str(r#"paginate = "auto""#).unwrap();
1040 assert_eq!(config.paginate, Some(PaginateSetting::Auto));
1041
1042 let config: DeserializedUiConfig = toml::from_str(r#"paginate = "never""#).unwrap();
1044 assert_eq!(config.paginate, Some(PaginateSetting::Never));
1045
1046 let config: DeserializedUiConfig = toml::from_str("").unwrap();
1048 assert!(config.paginate.is_none());
1049
1050 let err = toml::from_str::<DeserializedUiConfig>(r#"paginate = "invalid""#).unwrap_err();
1052 assert!(
1053 err.to_string().contains("unknown variant"),
1054 "error should mention 'unknown variant': {err}"
1055 );
1056 }
1057
1058 #[test]
1059 fn test_command_name_and_args_parsing() {
1060 #[derive(Debug, Deserialize)]
1061 struct Wrapper {
1062 cmd: CommandNameAndArgs,
1063 }
1064
1065 let wrapper: Wrapper = toml::from_str(r#"cmd = "less -FRX""#).unwrap();
1067 assert_eq!(
1068 wrapper.cmd,
1069 CommandNameAndArgs {
1070 command: vec!["less".to_owned(), "-FRX".to_owned()],
1071 env: BTreeMap::new(),
1072 }
1073 );
1074 assert_eq!(wrapper.cmd.command_name(), "less");
1075 assert_eq!(wrapper.cmd.args(), &["-FRX".to_owned()]);
1076
1077 let wrapper: Wrapper = toml::from_str(r#"cmd = ["less", "-F", "-R", "-X"]"#).unwrap();
1079 assert_eq!(
1080 wrapper.cmd,
1081 CommandNameAndArgs {
1082 command: vec![
1083 "less".to_owned(),
1084 "-F".to_owned(),
1085 "-R".to_owned(),
1086 "-X".to_owned()
1087 ],
1088 env: BTreeMap::new(),
1089 }
1090 );
1091 assert_eq!(wrapper.cmd.command_name(), "less");
1092 assert_eq!(
1093 wrapper.cmd.args(),
1094 &["-F".to_owned(), "-R".to_owned(), "-X".to_owned()]
1095 );
1096
1097 let cmd: CommandNameAndArgs = toml::from_str(
1099 r#"
1100 command = ["less", "-FRX"]
1101 env = { LESSCHARSET = "utf-8" }
1102 "#,
1103 )
1104 .unwrap();
1105 let expected_env: BTreeMap<String, String> =
1106 [("LESSCHARSET".to_owned(), "utf-8".to_owned())]
1107 .into_iter()
1108 .collect();
1109 assert_eq!(
1110 cmd,
1111 CommandNameAndArgs {
1112 command: vec!["less".to_owned(), "-FRX".to_owned()],
1113 env: expected_env,
1114 }
1115 );
1116 assert_eq!(cmd.command_name(), "less");
1117 assert_eq!(cmd.args(), &["-FRX".to_owned()]);
1118
1119 let wrapper: Wrapper = toml::from_str(r#"cmd = 'my-pager "arg with spaces"'"#).unwrap();
1121 assert_eq!(
1122 wrapper.cmd,
1123 CommandNameAndArgs {
1124 command: vec!["my-pager".to_owned(), "arg with spaces".to_owned()],
1125 env: BTreeMap::new(),
1126 }
1127 );
1128
1129 let wrapper: Wrapper = toml::from_str(r#"cmd = "my-pager 'arg with spaces'""#).unwrap();
1131 assert_eq!(
1132 wrapper.cmd,
1133 CommandNameAndArgs {
1134 command: vec!["my-pager".to_owned(), "arg with spaces".to_owned()],
1135 env: BTreeMap::new(),
1136 }
1137 );
1138
1139 let wrapper: Wrapper =
1141 toml::from_str(r#"cmd = 'my-pager "quoted \"nested\" arg"'"#).unwrap();
1142 assert_eq!(
1143 wrapper.cmd,
1144 CommandNameAndArgs {
1145 command: vec!["my-pager".to_owned(), "quoted \"nested\" arg".to_owned()],
1146 env: BTreeMap::new(),
1147 }
1148 );
1149
1150 let wrapper: Wrapper = toml::from_str(r#"cmd = '"/path/to/my pager" --flag'"#).unwrap();
1152 assert_eq!(
1153 wrapper.cmd,
1154 CommandNameAndArgs {
1155 command: vec!["/path/to/my pager".to_owned(), "--flag".to_owned()],
1156 env: BTreeMap::new(),
1157 }
1158 );
1159
1160 let wrapper: Wrapper =
1162 toml::from_str(r#"cmd = 'cmd "first arg" "second arg" third'"#).unwrap();
1163 assert_eq!(
1164 wrapper.cmd,
1165 CommandNameAndArgs {
1166 command: vec![
1167 "cmd".to_owned(),
1168 "first arg".to_owned(),
1169 "second arg".to_owned(),
1170 "third".to_owned(),
1171 ],
1172 env: BTreeMap::new(),
1173 }
1174 );
1175 }
1176
1177 #[test]
1178 fn test_command_and_pager_empty_errors() {
1179 #[derive(Debug, Deserialize)]
1180 struct Wrapper {
1181 #[expect(dead_code)]
1182 cmd: CommandNameAndArgs,
1183 }
1184
1185 let cmd_cases = [
1187 ("empty array", "cmd = []"),
1188 ("empty string", r#"cmd = """#),
1189 ("whitespace-only string", r#"cmd = " ""#),
1190 (
1191 "structured with empty command",
1192 r#"cmd = { command = [], env = { LESSCHARSET = "utf-8" } }"#,
1193 ),
1194 ];
1195
1196 for (name, input) in cmd_cases {
1197 let err = toml::from_str::<Wrapper>(input).unwrap_err();
1198 assert!(
1199 err.to_string().contains("must not be empty"),
1200 "CommandNameAndArgs {name}: error should mention 'must not be empty': {err}"
1201 );
1202 }
1203
1204 let pager_cases = [
1206 ("empty array", "pager = []"),
1207 ("empty string", r#"pager = """#),
1208 ];
1209
1210 for (name, input) in pager_cases {
1211 let err = toml::from_str::<DeserializedUiConfig>(input).unwrap_err();
1212 assert!(
1213 err.to_string().contains("must not be empty"),
1214 "PagerSetting {name}: error should mention 'must not be empty': {err}"
1215 );
1216 }
1217
1218 let unclosed_quote_cases = [
1220 ("unclosed double quote", r#"cmd = 'pager "unclosed'"#),
1221 ("unclosed single quote", r#"cmd = "pager 'unclosed""#),
1222 ];
1223
1224 for (name, input) in unclosed_quote_cases {
1225 let err = toml::from_str::<Wrapper>(input).unwrap_err();
1226 assert!(
1227 err.to_string().contains("missing closing quote"),
1228 "CommandNameAndArgs {name}: error should mention 'missing closing quote': {err}"
1229 );
1230 }
1231 }
1232
1233 #[test]
1234 fn test_command_name_and_args_to_command() {
1235 let cmd = CommandNameAndArgs {
1237 command: vec!["echo".to_owned(), "hello".to_owned()],
1238 env: BTreeMap::new(),
1239 };
1240 let std_cmd = cmd.to_command();
1241 assert_eq!(cmd.command_name(), "echo");
1242 drop(std_cmd);
1243 }
1244
1245 #[test]
1246 fn test_pager_setting_parsing() {
1247 let config: DeserializedUiConfig = toml::from_str(r#"pager = "less -FRX""#).unwrap();
1249 assert_eq!(
1250 config.pager,
1251 Some(PagerSetting::External(CommandNameAndArgs {
1252 command: vec!["less".to_owned(), "-FRX".to_owned()],
1253 env: BTreeMap::new(),
1254 }))
1255 );
1256
1257 let config: DeserializedUiConfig = toml::from_str(r#"pager = ["less", "-FRX"]"#).unwrap();
1259 assert_eq!(
1260 config.pager,
1261 Some(PagerSetting::External(CommandNameAndArgs {
1262 command: vec!["less".to_owned(), "-FRX".to_owned()],
1263 env: BTreeMap::new(),
1264 }))
1265 );
1266
1267 let config: DeserializedUiConfig = toml::from_str(
1269 r#"
1270 [pager]
1271 command = ["less", "-FRX"]
1272 env = { LESSCHARSET = "utf-8" }
1273 "#,
1274 )
1275 .unwrap();
1276 let expected_env: BTreeMap<String, String> =
1277 [("LESSCHARSET".to_owned(), "utf-8".to_owned())]
1278 .into_iter()
1279 .collect();
1280 assert_eq!(
1281 config.pager,
1282 Some(PagerSetting::External(CommandNameAndArgs {
1283 command: vec!["less".to_owned(), "-FRX".to_owned()],
1284 env: expected_env,
1285 }))
1286 );
1287
1288 let config: DeserializedUiConfig = toml::from_str("").unwrap();
1290 assert!(config.pager.is_none());
1291 }
1292
1293 #[test]
1294 fn test_resolved_ui_config_pager_defaults() {
1295 let defaults = DefaultUserConfig::from_embedded().ui;
1296
1297 let host = detect_host_platform_for_tests();
1298 let resolved = UiConfig::resolve(&defaults, &[], None, &[], &host);
1299
1300 assert_eq!(resolved.pager, defaults.pager);
1302 assert_eq!(resolved.paginate, defaults.paginate);
1303 }
1304
1305 #[test]
1306 fn test_resolved_ui_config_pager_override() {
1307 let defaults = DefaultUserConfig::from_embedded().ui;
1308
1309 let custom_pager = PagerSetting::External(CommandNameAndArgs {
1311 command: vec!["more".to_owned()],
1312 env: BTreeMap::new(),
1313 });
1314 let override_ = make_override(
1315 "cfg(all())",
1316 DeserializedUiOverrideData {
1317 pager: Some(custom_pager.clone()),
1318 ..Default::default()
1319 },
1320 );
1321
1322 let host = detect_host_platform_for_tests();
1323 let resolved = UiConfig::resolve(&defaults, &[], None, &[override_], &host);
1324
1325 assert_eq!(resolved.pager, custom_pager);
1326 assert_eq!(resolved.paginate, defaults.paginate);
1328 }
1329
1330 #[test]
1331 fn test_resolved_ui_config_paginate_override() {
1332 let defaults = DefaultUserConfig::from_embedded().ui;
1333
1334 let override_ = make_override(
1336 "cfg(all())",
1337 DeserializedUiOverrideData {
1338 paginate: Some(PaginateSetting::Never),
1339 ..Default::default()
1340 },
1341 );
1342
1343 let host = detect_host_platform_for_tests();
1344 let resolved = UiConfig::resolve(&defaults, &[], None, &[override_], &host);
1345
1346 assert_eq!(resolved.paginate, PaginateSetting::Never);
1347 assert_eq!(resolved.pager, defaults.pager);
1349 }
1350
1351 #[test]
1352 fn test_pager_setting_builtin() {
1353 let config: DeserializedUiConfig = toml::from_str(r#"pager = ":builtin""#).unwrap();
1355 assert_eq!(config.pager, Some(PagerSetting::Builtin));
1356 }
1357
1358 #[test]
1359 fn test_streampager_config_parsing() {
1360 let config: DeserializedUiConfig = toml::from_str(
1362 r#"
1363 [streampager]
1364 interface = "full-screen-clear-output"
1365 wrapping = "anywhere"
1366 show-ruler = false
1367 "#,
1368 )
1369 .unwrap();
1370 assert_eq!(
1371 config.streampager.interface,
1372 Some(StreampagerInterface::FullScreenClearOutput)
1373 );
1374 assert_eq!(
1375 config.streampager.wrapping,
1376 Some(StreampagerWrapping::Anywhere)
1377 );
1378 assert_eq!(config.streampager.show_ruler, Some(false));
1379
1380 let config: DeserializedUiConfig = toml::from_str(
1382 r#"
1383 [streampager]
1384 interface = "quit-quickly-or-clear-output"
1385 "#,
1386 )
1387 .unwrap();
1388 assert_eq!(
1389 config.streampager.interface,
1390 Some(StreampagerInterface::QuitQuicklyOrClearOutput)
1391 );
1392 assert_eq!(config.streampager.wrapping, None);
1393 assert_eq!(config.streampager.show_ruler, None);
1394
1395 let config: DeserializedUiConfig = toml::from_str("").unwrap();
1397 assert_eq!(config.streampager.interface, None);
1398 assert_eq!(config.streampager.wrapping, None);
1399 assert_eq!(config.streampager.show_ruler, None);
1400 }
1401
1402 #[test]
1403 fn test_streampager_config_resolution() {
1404 let defaults = DefaultUserConfig::from_embedded().ui;
1405
1406 let override_ = make_override(
1408 "cfg(all())",
1409 DeserializedUiOverrideData {
1410 streampager: DeserializedStreampagerConfig {
1411 interface: Some(StreampagerInterface::FullScreenClearOutput),
1412 wrapping: None,
1413 show_ruler: None,
1414 },
1415 ..Default::default()
1416 },
1417 );
1418
1419 let host = detect_host_platform_for_tests();
1420 let resolved = UiConfig::resolve(&defaults, &[], None, &[override_], &host);
1421
1422 assert_eq!(
1424 resolved.streampager.interface,
1425 StreampagerInterface::FullScreenClearOutput
1426 );
1427 assert_eq!(resolved.streampager.wrapping, defaults.streampager.wrapping);
1429 assert_eq!(
1430 resolved.streampager.show_ruler,
1431 defaults.streampager.show_ruler
1432 );
1433 }
1434}