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)]
19#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
20#[cfg_attr(feature = "config-schema", schemars(deny_unknown_fields))]
21#[serde(rename_all = "kebab-case")]
22pub(in crate::user_config) struct DeserializedUiConfig {
23 pub(in crate::user_config) show_progress: Option<UiShowProgress>,
25
26 #[serde(default, deserialize_with = "deserialize_max_progress_running")]
32 max_progress_running: Option<MaxProgressRunning>,
33
34 input_handler: Option<bool>,
37
38 output_indent: Option<bool>,
40
41 #[serde(default)]
44 pager: Option<PagerSetting>,
45
46 #[serde(default)]
48 paginate: Option<PaginateSetting>,
49
50 #[serde(default)]
52 streampager: DeserializedStreampagerConfig,
53}
54
55#[derive(Clone, Debug, Deserialize)]
60#[serde(rename_all = "kebab-case")]
61pub(crate) struct DefaultUiConfig {
62 show_progress: UiShowProgress,
64
65 #[serde(deserialize_with = "deserialize_max_progress_running_required")]
67 max_progress_running: MaxProgressRunning,
68
69 input_handler: bool,
71
72 output_indent: bool,
74
75 pub(in crate::user_config) pager: PagerSetting,
77
78 pub(in crate::user_config) paginate: PaginateSetting,
80
81 pub(in crate::user_config) streampager: DefaultStreampagerConfig,
83}
84
85#[derive(Clone, Debug, Default, Deserialize)]
90#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
91#[cfg_attr(feature = "config-schema", schemars(deny_unknown_fields))]
92#[serde(rename_all = "kebab-case")]
93pub(in crate::user_config) struct DeserializedUiOverrideData {
94 pub(in crate::user_config) show_progress: Option<UiShowProgress>,
96
97 #[serde(default, deserialize_with = "deserialize_max_progress_running")]
99 pub(in crate::user_config) max_progress_running: Option<MaxProgressRunning>,
100
101 pub(in crate::user_config) input_handler: Option<bool>,
103
104 pub(in crate::user_config) output_indent: Option<bool>,
106
107 #[serde(default)]
109 pub(in crate::user_config) pager: Option<PagerSetting>,
110
111 #[serde(default)]
113 pub(in crate::user_config) paginate: Option<PaginateSetting>,
114
115 #[serde(default)]
117 pub(in crate::user_config) streampager: DeserializedStreampagerConfig,
118}
119
120#[derive(Clone, Debug)]
125pub(in crate::user_config) struct CompiledUiOverride {
126 platform_spec: TargetSpec,
127 data: UiOverrideData,
128}
129
130impl CompiledUiOverride {
131 pub(in crate::user_config) fn new(
133 platform_spec: TargetSpec,
134 data: DeserializedUiOverrideData,
135 ) -> Self {
136 Self {
137 platform_spec,
138 data: UiOverrideData {
139 show_progress: data.show_progress,
140 max_progress_running: data.max_progress_running,
141 input_handler: data.input_handler,
142 output_indent: data.output_indent,
143 pager: data.pager,
144 paginate: data.paginate,
145 streampager_interface: data.streampager.interface,
146 streampager_wrapping: data.streampager.wrapping,
147 streampager_show_ruler: data.streampager.show_ruler,
148 },
149 }
150 }
151
152 pub(in crate::user_config) fn matches(&self, build_target: &Platform) -> bool {
157 self.platform_spec
158 .eval(build_target)
159 .unwrap_or(false)
160 }
161
162 pub(in crate::user_config) fn data(&self) -> &UiOverrideData {
164 &self.data
165 }
166}
167
168#[derive(Clone, Debug, Default)]
170pub(in crate::user_config) struct UiOverrideData {
171 show_progress: Option<UiShowProgress>,
172 max_progress_running: Option<MaxProgressRunning>,
173 input_handler: Option<bool>,
174 output_indent: Option<bool>,
175 pager: Option<PagerSetting>,
176 paginate: Option<PaginateSetting>,
177 streampager_interface: Option<StreampagerInterface>,
178 streampager_wrapping: Option<StreampagerWrapping>,
179 streampager_show_ruler: Option<bool>,
180}
181
182impl UiOverrideData {
183 pub(in crate::user_config) fn pager(&self) -> Option<&PagerSetting> {
185 self.pager.as_ref()
186 }
187
188 pub(in crate::user_config) fn paginate(&self) -> Option<&PaginateSetting> {
190 self.paginate.as_ref()
191 }
192
193 pub(in crate::user_config) fn streampager_interface(&self) -> Option<&StreampagerInterface> {
195 self.streampager_interface.as_ref()
196 }
197
198 pub(in crate::user_config) fn streampager_wrapping(&self) -> Option<&StreampagerWrapping> {
200 self.streampager_wrapping.as_ref()
201 }
202
203 pub(in crate::user_config) fn streampager_show_ruler(&self) -> Option<&bool> {
205 self.streampager_show_ruler.as_ref()
206 }
207}
208
209#[derive(Clone, Debug)]
214pub struct UiConfig {
215 pub show_progress: UiShowProgress,
217 pub max_progress_running: MaxProgressRunning,
219 pub input_handler: bool,
221 pub output_indent: bool,
223 pub pager: PagerSetting,
225 pub paginate: PaginateSetting,
227 pub streampager: StreampagerConfig,
229}
230
231impl UiConfig {
232 pub(in crate::user_config) fn resolve(
244 default_config: &DefaultUiConfig,
245 default_overrides: &[CompiledUiOverride],
246 user_config: Option<&DeserializedUiConfig>,
247 user_overrides: &[CompiledUiOverride],
248 build_target: &Platform,
249 ) -> Self {
250 Self {
251 show_progress: resolve_ui_setting(
252 &default_config.show_progress,
253 default_overrides,
254 user_config.and_then(|c| c.show_progress.as_ref()),
255 user_overrides,
256 build_target,
257 |data| data.show_progress.as_ref(),
258 ),
259 max_progress_running: resolve_ui_setting(
260 &default_config.max_progress_running,
261 default_overrides,
262 user_config.and_then(|c| c.max_progress_running.as_ref()),
263 user_overrides,
264 build_target,
265 |data| data.max_progress_running.as_ref(),
266 ),
267 input_handler: resolve_ui_setting(
268 &default_config.input_handler,
269 default_overrides,
270 user_config.and_then(|c| c.input_handler.as_ref()),
271 user_overrides,
272 build_target,
273 |data| data.input_handler.as_ref(),
274 ),
275 output_indent: resolve_ui_setting(
276 &default_config.output_indent,
277 default_overrides,
278 user_config.and_then(|c| c.output_indent.as_ref()),
279 user_overrides,
280 build_target,
281 |data| data.output_indent.as_ref(),
282 ),
283 pager: resolve_ui_setting(
284 &default_config.pager,
285 default_overrides,
286 user_config.and_then(|c| c.pager.as_ref()),
287 user_overrides,
288 build_target,
289 |data| data.pager.as_ref(),
290 ),
291 paginate: resolve_ui_setting(
292 &default_config.paginate,
293 default_overrides,
294 user_config.and_then(|c| c.paginate.as_ref()),
295 user_overrides,
296 build_target,
297 |data| data.paginate.as_ref(),
298 ),
299 streampager: StreampagerConfig {
300 interface: resolve_ui_setting(
301 &default_config.streampager.interface,
302 default_overrides,
303 user_config.and_then(|c| c.streampager.interface.as_ref()),
304 user_overrides,
305 build_target,
306 |data| data.streampager_interface.as_ref(),
307 ),
308 wrapping: resolve_ui_setting(
309 &default_config.streampager.wrapping,
310 default_overrides,
311 user_config.and_then(|c| c.streampager.wrapping.as_ref()),
312 user_overrides,
313 build_target,
314 |data| data.streampager_wrapping.as_ref(),
315 ),
316 show_ruler: resolve_ui_setting(
317 &default_config.streampager.show_ruler,
318 default_overrides,
319 user_config.and_then(|c| c.streampager.show_ruler.as_ref()),
320 user_overrides,
321 build_target,
322 |data| data.streampager_show_ruler.as_ref(),
323 ),
324 },
325 }
326 }
327}
328
329#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
331#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
332#[serde(rename_all = "kebab-case")]
333pub enum UiShowProgress {
334 #[default]
337 Auto,
338 None,
340 Bar,
342 Counter,
344 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#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
370#[serde(rename_all = "kebab-case")]
371pub enum PaginateSetting {
372 #[default]
374 Auto,
375 Never,
377}
378
379pub const BUILTIN_PAGER_NAME: &str = ":builtin";
381
382#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
384#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
385#[cfg_attr(feature = "config-schema", schemars(deny_unknown_fields))]
386#[serde(rename_all = "kebab-case")]
387pub(in crate::user_config) struct DeserializedStreampagerConfig {
388 pub(in crate::user_config) interface: Option<StreampagerInterface>,
390 pub(in crate::user_config) wrapping: Option<StreampagerWrapping>,
392 pub(in crate::user_config) show_ruler: Option<bool>,
394}
395
396#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
400#[serde(rename_all = "kebab-case")]
401pub(in crate::user_config) struct DefaultStreampagerConfig {
402 pub(in crate::user_config) interface: StreampagerInterface,
404 pub(in crate::user_config) wrapping: StreampagerWrapping,
406 pub(in crate::user_config) show_ruler: bool,
408}
409
410#[derive(Clone, Copy, Debug, PartialEq, Eq)]
414pub struct StreampagerConfig {
415 pub interface: StreampagerInterface,
417 pub wrapping: StreampagerWrapping,
419 pub show_ruler: bool,
421}
422
423impl StreampagerConfig {
424 pub fn streampager_interface_mode(&self) -> streampager::config::InterfaceMode {
426 use streampager::config::InterfaceMode;
427 match self.interface {
428 StreampagerInterface::FullScreenClearOutput => InterfaceMode::FullScreen,
429 StreampagerInterface::QuitIfOnePage => InterfaceMode::Hybrid,
430 StreampagerInterface::QuitQuicklyOrClearOutput => {
431 InterfaceMode::Delayed(std::time::Duration::from_secs(2))
432 }
433 }
434 }
435
436 pub fn streampager_wrapping_mode(&self) -> streampager::config::WrappingMode {
438 use streampager::config::WrappingMode;
439 match self.wrapping {
440 StreampagerWrapping::None => WrappingMode::Unwrapped,
441 StreampagerWrapping::Word => WrappingMode::WordBoundary,
442 StreampagerWrapping::Anywhere => WrappingMode::GraphemeBoundary,
443 }
444 }
445}
446
447#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
449#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
450#[serde(rename_all = "kebab-case")]
451pub enum StreampagerInterface {
452 #[default]
455 QuitIfOnePage,
456 FullScreenClearOutput,
458 QuitQuicklyOrClearOutput,
461}
462
463#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
465#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))]
466#[serde(rename_all = "kebab-case")]
467pub enum StreampagerWrapping {
468 None,
471 #[default]
473 Word,
474 Anywhere,
476}
477
478#[derive(Clone, Debug, PartialEq, Eq)]
486pub struct CommandNameAndArgs {
487 command: Vec<String>,
489 env: BTreeMap<String, String>,
491}
492
493impl CommandNameAndArgs {
494 pub fn command_name(&self) -> &str {
496 &self.command[0]
498 }
499
500 pub fn args(&self) -> &[String] {
502 &self.command[1..]
503 }
504
505 pub fn to_command(&self) -> Command {
507 let mut cmd = Command::new(self.command_name());
508 cmd.args(self.args());
509 cmd.envs(&self.env);
510 cmd
511 }
512}
513
514impl<'de> Deserialize<'de> for CommandNameAndArgs {
515 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
516 deserializer.deserialize_any(CommandNameAndArgsVisitor)
517 }
518}
519
520#[cfg(feature = "config-schema")]
521impl schemars::JsonSchema for CommandNameAndArgs {
522 fn schema_name() -> std::borrow::Cow<'static, str> {
523 "CommandNameAndArgs".into()
524 }
525
526 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
527 schemars::json_schema!({
532 "title": "CommandNameAndArgs",
533 "oneOf": [
534 {
535 "type": "string",
536 "minLength": 1,
537 "pattern": "\\S",
538 },
539 {
540 "type": "array",
541 "items": generator.subschema_for::<String>(),
542 "minItems": 1,
543 },
544 {
545 "type": "object",
546 "properties": {
547 "command": {
548 "type": "array",
549 "items": generator.subschema_for::<String>(),
550 "minItems": 1,
551 },
552 "env": generator.subschema_for::<BTreeMap<String, String>>(),
553 },
554 "required": ["command"],
555 "additionalProperties": false,
556 }
557 ]
558 })
559 }
560}
561
562struct CommandNameAndArgsVisitor;
564
565impl<'de> de::Visitor<'de> for CommandNameAndArgsVisitor {
566 type Value = CommandNameAndArgs;
567
568 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
569 formatter.write_str(
570 "a command string (\"less -FRX\"), \
571 an array ([\"less\", \"-FRX\"]), \
572 or a table ({ command = [\"less\", \"-FRX\"], env = { ... } })",
573 )
574 }
575
576 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
577 let command: Vec<String> = shell_words::split(v).map_err(de::Error::custom)?;
578 if command.is_empty() {
579 return Err(de::Error::custom("command string must not be empty"));
580 }
581 Ok(CommandNameAndArgs {
582 command,
583 env: BTreeMap::new(),
584 })
585 }
586
587 fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
588 let mut command = Vec::new();
589 while let Some(arg) = seq.next_element::<String>()? {
590 command.push(arg);
591 }
592 if command.is_empty() {
593 return Err(de::Error::custom("command array must not be empty"));
594 }
595 Ok(CommandNameAndArgs {
596 command,
597 env: BTreeMap::new(),
598 })
599 }
600
601 fn visit_map<A: de::MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
602 #[derive(Deserialize)]
603 struct StructuredInner {
604 command: Vec<String>,
605 #[serde(default)]
606 env: BTreeMap<String, String>,
607 }
608
609 let inner = StructuredInner::deserialize(de::value::MapAccessDeserializer::new(map))?;
610 if inner.command.is_empty() {
611 return Err(de::Error::custom("command array must not be empty"));
612 }
613 Ok(CommandNameAndArgs {
614 command: inner.command,
615 env: inner.env,
616 })
617 }
618}
619
620#[derive(Clone, Debug, PartialEq, Eq)]
625pub enum PagerSetting {
626 Builtin,
628 External(CommandNameAndArgs),
630}
631
632#[cfg(test)]
635impl Default for PagerSetting {
636 fn default() -> Self {
637 Self::External(CommandNameAndArgs {
638 command: vec!["less".to_owned(), "-FRX".to_owned()],
639 env: [("LESSCHARSET".to_owned(), "utf-8".to_owned())]
640 .into_iter()
641 .collect(),
642 })
643 }
644}
645
646impl<'de> Deserialize<'de> for PagerSetting {
647 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
648 deserializer.deserialize_any(PagerSettingVisitor)
649 }
650}
651
652#[cfg(feature = "config-schema")]
653impl schemars::JsonSchema for PagerSetting {
654 fn schema_name() -> std::borrow::Cow<'static, str> {
655 "PagerSetting".into()
656 }
657
658 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
659 schemars::json_schema!({
665 "title": "PagerSetting",
666 "anyOf": [
667 { "type": "string", "const": BUILTIN_PAGER_NAME },
668 generator.subschema_for::<CommandNameAndArgs>(),
669 ]
670 })
671 }
672}
673
674struct PagerSettingVisitor;
676
677impl<'de> de::Visitor<'de> for PagerSettingVisitor {
678 type Value = PagerSetting;
679
680 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
681 formatter
682 .write_str("\":builtin\", a command string, an array, or a table with command and env")
683 }
684
685 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
686 if v == BUILTIN_PAGER_NAME {
688 return Ok(PagerSetting::Builtin);
689 }
690 let cmd = CommandNameAndArgsVisitor.visit_str(v)?;
691 Ok(PagerSetting::External(cmd))
692 }
693
694 fn visit_seq<A: de::SeqAccess<'de>>(self, seq: A) -> Result<Self::Value, A::Error> {
695 let args = CommandNameAndArgsVisitor.visit_seq(seq)?;
696 Ok(PagerSetting::External(args))
697 }
698
699 fn visit_map<A: de::MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
700 let args = CommandNameAndArgsVisitor.visit_map(map)?;
701 Ok(PagerSetting::External(args))
702 }
703}
704
705struct MaxProgressRunningVisitor;
707
708impl<'de> de::Visitor<'de> for MaxProgressRunningVisitor {
709 type Value = MaxProgressRunning;
710
711 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
712 formatter.write_str("a non-negative integer or \"infinite\"")
713 }
714
715 fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
716 Ok(MaxProgressRunning::Count(v as usize))
717 }
718
719 fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
720 if v < 0 {
721 Err(E::invalid_value(Unexpected::Signed(v), &self))
722 } else {
723 Ok(MaxProgressRunning::Count(v as usize))
724 }
725 }
726
727 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
728 if v == "infinite" {
729 Ok(MaxProgressRunning::Infinite)
730 } else {
731 Err(E::invalid_value(Unexpected::Str(v), &self))
732 }
733 }
734}
735
736fn deserialize_max_progress_running<'de, D>(
737 deserializer: D,
738) -> Result<Option<MaxProgressRunning>, D::Error>
739where
740 D: Deserializer<'de>,
741{
742 deserializer.deserialize_option(OptionMaxProgressRunningVisitor)
743}
744
745struct OptionMaxProgressRunningVisitor;
747
748impl<'de> de::Visitor<'de> for OptionMaxProgressRunningVisitor {
749 type Value = Option<MaxProgressRunning>;
750
751 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
752 formatter.write_str("a non-negative integer, \"infinite\", or null")
753 }
754
755 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
756 Ok(None)
757 }
758
759 fn visit_some<D: Deserializer<'de>>(self, deserializer: D) -> Result<Self::Value, D::Error> {
760 deserializer
761 .deserialize_any(MaxProgressRunningVisitor)
762 .map(Some)
763 }
764
765 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
766 Ok(None)
767 }
768}
769
770fn deserialize_max_progress_running_required<'de, D>(
771 deserializer: D,
772) -> Result<MaxProgressRunning, D::Error>
773where
774 D: Deserializer<'de>,
775{
776 deserializer.deserialize_any(MaxProgressRunningVisitor)
777}
778
779#[cfg(test)]
780mod tests {
781 use super::*;
782 use crate::user_config::DefaultUserConfig;
783
784 fn make_override(platform: &str, data: DeserializedUiOverrideData) -> CompiledUiOverride {
786 let platform_spec =
787 TargetSpec::new(platform.to_string()).expect("valid platform spec in test");
788 CompiledUiOverride::new(platform_spec, data)
789 }
790
791 #[test]
792 fn test_ui_config_show_progress() {
793 let config: DeserializedUiConfig = toml::from_str(r#"show-progress = "auto""#).unwrap();
795 assert!(matches!(config.show_progress, Some(UiShowProgress::Auto)));
796
797 let config: DeserializedUiConfig = toml::from_str(r#"show-progress = "none""#).unwrap();
798 assert!(matches!(config.show_progress, Some(UiShowProgress::None)));
799
800 let config: DeserializedUiConfig = toml::from_str(r#"show-progress = "bar""#).unwrap();
801 assert!(matches!(config.show_progress, Some(UiShowProgress::Bar)));
802
803 let config: DeserializedUiConfig = toml::from_str(r#"show-progress = "counter""#).unwrap();
804 assert!(matches!(
805 config.show_progress,
806 Some(UiShowProgress::Counter)
807 ));
808
809 let config: DeserializedUiConfig = toml::from_str(r#"show-progress = "only""#).unwrap();
810 assert!(matches!(config.show_progress, Some(UiShowProgress::Only)));
811
812 let config: DeserializedUiConfig = toml::from_str("").unwrap();
814 assert!(config.show_progress.is_none());
815
816 toml::from_str::<DeserializedUiConfig>(r#"show-progress = "invalid""#).unwrap_err();
818 }
819
820 #[test]
821 fn test_ui_show_progress_to_show_progress() {
822 assert_eq!(
824 ShowProgress::from(UiShowProgress::Auto),
825 ShowProgress::Auto {
826 suppress_success: false
827 }
828 );
829 assert_eq!(ShowProgress::from(UiShowProgress::None), ShowProgress::None);
830 assert_eq!(
831 ShowProgress::from(UiShowProgress::Bar),
832 ShowProgress::Running
833 );
834 assert_eq!(
835 ShowProgress::from(UiShowProgress::Counter),
836 ShowProgress::Counter
837 );
838 assert_eq!(
841 ShowProgress::from(UiShowProgress::Only),
842 ShowProgress::Auto {
843 suppress_success: true
844 }
845 );
846 }
847
848 #[test]
849 fn test_ui_config_max_progress_running() {
850 let config: DeserializedUiConfig = toml::from_str("max-progress-running = 10").unwrap();
852 assert!(matches!(
853 config.max_progress_running,
854 Some(MaxProgressRunning::Count(10))
855 ));
856
857 let config: DeserializedUiConfig = toml::from_str("max-progress-running = 0").unwrap();
858 assert!(matches!(
859 config.max_progress_running,
860 Some(MaxProgressRunning::Count(0))
861 ));
862
863 let config: DeserializedUiConfig =
865 toml::from_str(r#"max-progress-running = "infinite""#).unwrap();
866 assert!(matches!(
867 config.max_progress_running,
868 Some(MaxProgressRunning::Infinite)
869 ));
870
871 toml::from_str::<DeserializedUiConfig>(r#"max-progress-running = "INFINITE""#).unwrap_err();
873
874 toml::from_str::<DeserializedUiConfig>(r#"max-progress-running = "8""#).unwrap_err();
876
877 let config: DeserializedUiConfig = toml::from_str("").unwrap();
879 assert!(config.max_progress_running.is_none());
880
881 toml::from_str::<DeserializedUiConfig>(r#"max-progress-running = "invalid""#).unwrap_err();
883 }
884
885 #[test]
886 fn test_ui_config_input_handler() {
887 let config: DeserializedUiConfig = toml::from_str("input-handler = true").unwrap();
888 assert_eq!(config.input_handler, Some(true));
889 let config: DeserializedUiConfig = toml::from_str("input-handler = false").unwrap();
890 assert_eq!(config.input_handler, Some(false));
891 let config: DeserializedUiConfig = toml::from_str("").unwrap();
892 assert!(config.input_handler.is_none());
893 }
894
895 #[test]
896 fn test_ui_config_output_indent() {
897 let config: DeserializedUiConfig = toml::from_str("output-indent = true").unwrap();
898 assert_eq!(config.output_indent, Some(true));
899 let config: DeserializedUiConfig = toml::from_str("output-indent = false").unwrap();
900 assert_eq!(config.output_indent, Some(false));
901 let config: DeserializedUiConfig = toml::from_str("").unwrap();
902 assert!(config.output_indent.is_none());
903 }
904
905 #[test]
906 fn test_resolved_ui_config_defaults_only() {
907 let defaults = DefaultUserConfig::from_embedded().ui;
908
909 let build_target =
910 Platform::build_target().expect("nextest is built for a supported platform");
911 let resolved = UiConfig::resolve(&defaults, &[], None, &[], &build_target);
912
913 assert_eq!(resolved.show_progress, defaults.show_progress);
915 assert_eq!(resolved.max_progress_running, defaults.max_progress_running);
916 assert_eq!(resolved.input_handler, defaults.input_handler);
917 assert_eq!(resolved.output_indent, defaults.output_indent);
918 }
919
920 #[test]
921 fn test_resolved_ui_config_user_config_overrides_defaults() {
922 let defaults = DefaultUserConfig::from_embedded().ui;
923
924 let user_config = DeserializedUiConfig {
925 show_progress: Some(UiShowProgress::Bar),
926 max_progress_running: Some(MaxProgressRunning::Count(4)),
927 output_indent: Some(false),
928 ..Default::default()
929 };
930
931 let build_target =
932 Platform::build_target().expect("nextest is built for a supported platform");
933 let resolved = UiConfig::resolve(&defaults, &[], Some(&user_config), &[], &build_target);
934
935 assert_eq!(resolved.show_progress, UiShowProgress::Bar);
936 assert_eq!(resolved.max_progress_running, MaxProgressRunning::Count(4));
937 assert_eq!(resolved.input_handler, defaults.input_handler); assert!(!resolved.output_indent);
939 }
940
941 #[test]
942 fn test_resolved_ui_config_user_override_applies() {
943 let defaults = DefaultUserConfig::from_embedded().ui;
944
945 let override_ = make_override(
947 "cfg(all())",
948 DeserializedUiOverrideData {
949 show_progress: Some(UiShowProgress::Counter),
950 input_handler: Some(false),
951 ..Default::default()
952 },
953 );
954
955 let build_target =
956 Platform::build_target().expect("nextest is built for a supported platform");
957 let resolved = UiConfig::resolve(&defaults, &[], None, &[override_], &build_target);
958
959 assert_eq!(resolved.show_progress, UiShowProgress::Counter);
960 assert_eq!(resolved.max_progress_running, defaults.max_progress_running); assert!(!resolved.input_handler);
962 assert_eq!(resolved.output_indent, defaults.output_indent); }
964
965 #[test]
966 fn test_resolved_ui_config_default_override_applies() {
967 let defaults = DefaultUserConfig::from_embedded().ui;
968
969 let override_ = make_override(
971 "cfg(all())",
972 DeserializedUiOverrideData {
973 show_progress: Some(UiShowProgress::Counter),
974 input_handler: Some(false),
975 ..Default::default()
976 },
977 );
978
979 let build_target =
980 Platform::build_target().expect("nextest is built for a supported platform");
981 let resolved = UiConfig::resolve(&defaults, &[override_], None, &[], &build_target);
982
983 assert_eq!(resolved.show_progress, UiShowProgress::Counter);
984 assert_eq!(resolved.max_progress_running, defaults.max_progress_running); assert!(!resolved.input_handler);
986 assert_eq!(resolved.output_indent, defaults.output_indent); }
988
989 #[test]
990 fn test_resolved_ui_config_platform_override_no_match() {
991 let defaults = DefaultUserConfig::from_embedded().ui;
992
993 let override_ = make_override(
996 "cfg(any())",
997 DeserializedUiOverrideData {
998 show_progress: Some(UiShowProgress::Counter),
999 max_progress_running: Some(MaxProgressRunning::Count(2)),
1000 input_handler: Some(false),
1001 output_indent: Some(false),
1002 pager: Some(PagerSetting::default()),
1003 paginate: Some(PaginateSetting::Never),
1004 streampager: Default::default(),
1005 },
1006 );
1007
1008 let build_target =
1009 Platform::build_target().expect("nextest is built for a supported platform");
1010 let resolved = UiConfig::resolve(&defaults, &[], None, &[override_], &build_target);
1011
1012 assert_eq!(resolved.show_progress, defaults.show_progress);
1014 assert_eq!(resolved.max_progress_running, defaults.max_progress_running);
1015 assert_eq!(resolved.input_handler, defaults.input_handler);
1016 assert_eq!(resolved.output_indent, defaults.output_indent);
1017 }
1018
1019 #[test]
1020 fn test_resolved_ui_config_first_matching_user_override_wins() {
1021 let defaults = DefaultUserConfig::from_embedded().ui;
1022
1023 let override1 = make_override(
1025 "cfg(all())",
1026 DeserializedUiOverrideData {
1027 show_progress: Some(UiShowProgress::Bar),
1028 ..Default::default()
1029 },
1030 );
1031
1032 let override2 = make_override(
1033 "cfg(all())",
1034 DeserializedUiOverrideData {
1035 show_progress: Some(UiShowProgress::Counter), max_progress_running: Some(MaxProgressRunning::Count(4)),
1037 ..Default::default()
1038 },
1039 );
1040
1041 let build_target =
1042 Platform::build_target().expect("nextest is built for a supported platform");
1043 let resolved =
1044 UiConfig::resolve(&defaults, &[], None, &[override1, override2], &build_target);
1045
1046 assert_eq!(resolved.show_progress, UiShowProgress::Bar);
1048 assert_eq!(resolved.max_progress_running, MaxProgressRunning::Count(4));
1050 }
1051
1052 #[test]
1053 fn test_resolved_ui_config_user_override_beats_default_override() {
1054 let defaults = DefaultUserConfig::from_embedded().ui;
1055
1056 let user_override = make_override(
1058 "cfg(all())",
1059 DeserializedUiOverrideData {
1060 show_progress: Some(UiShowProgress::Bar),
1061 ..Default::default()
1062 },
1063 );
1064
1065 let default_override = make_override(
1067 "cfg(all())",
1068 DeserializedUiOverrideData {
1069 show_progress: Some(UiShowProgress::Counter), max_progress_running: Some(MaxProgressRunning::Count(4)),
1071 ..Default::default()
1072 },
1073 );
1074
1075 let build_target =
1076 Platform::build_target().expect("nextest is built for a supported platform");
1077 let resolved = UiConfig::resolve(
1078 &defaults,
1079 &[default_override],
1080 None,
1081 &[user_override],
1082 &build_target,
1083 );
1084
1085 assert_eq!(resolved.show_progress, UiShowProgress::Bar);
1087 assert_eq!(resolved.max_progress_running, MaxProgressRunning::Count(4));
1089 }
1090
1091 #[test]
1092 fn test_resolved_ui_config_override_beats_user_base() {
1093 let defaults = DefaultUserConfig::from_embedded().ui;
1094
1095 let user_config = DeserializedUiConfig {
1097 show_progress: Some(UiShowProgress::None),
1098 max_progress_running: Some(MaxProgressRunning::Count(2)),
1099 ..Default::default()
1100 };
1101
1102 let default_override = make_override(
1104 "cfg(all())",
1105 DeserializedUiOverrideData {
1106 show_progress: Some(UiShowProgress::Counter),
1107 ..Default::default()
1108 },
1109 );
1110
1111 let build_target =
1112 Platform::build_target().expect("nextest is built for a supported platform");
1113 let resolved = UiConfig::resolve(
1114 &defaults,
1115 &[default_override],
1116 Some(&user_config),
1117 &[],
1118 &build_target,
1119 );
1120
1121 assert_eq!(resolved.show_progress, UiShowProgress::Counter);
1123 assert_eq!(resolved.max_progress_running, MaxProgressRunning::Count(2));
1125 }
1126
1127 #[test]
1128 fn test_paginate_setting_parsing() {
1129 let config: DeserializedUiConfig = toml::from_str(r#"paginate = "auto""#).unwrap();
1131 assert_eq!(config.paginate, Some(PaginateSetting::Auto));
1132
1133 let config: DeserializedUiConfig = toml::from_str(r#"paginate = "never""#).unwrap();
1135 assert_eq!(config.paginate, Some(PaginateSetting::Never));
1136
1137 let config: DeserializedUiConfig = toml::from_str("").unwrap();
1139 assert!(config.paginate.is_none());
1140
1141 let err = toml::from_str::<DeserializedUiConfig>(r#"paginate = "invalid""#).unwrap_err();
1143 assert!(
1144 err.to_string().contains("unknown variant"),
1145 "error should mention 'unknown variant': {err}"
1146 );
1147 }
1148
1149 #[test]
1150 fn test_command_name_and_args_parsing() {
1151 #[derive(Debug, Deserialize)]
1152 struct Wrapper {
1153 cmd: CommandNameAndArgs,
1154 }
1155
1156 let wrapper: Wrapper = toml::from_str(r#"cmd = "less -FRX""#).unwrap();
1158 assert_eq!(
1159 wrapper.cmd,
1160 CommandNameAndArgs {
1161 command: vec!["less".to_owned(), "-FRX".to_owned()],
1162 env: BTreeMap::new(),
1163 }
1164 );
1165 assert_eq!(wrapper.cmd.command_name(), "less");
1166 assert_eq!(wrapper.cmd.args(), &["-FRX".to_owned()]);
1167
1168 let wrapper: Wrapper = toml::from_str(r#"cmd = ["less", "-F", "-R", "-X"]"#).unwrap();
1170 assert_eq!(
1171 wrapper.cmd,
1172 CommandNameAndArgs {
1173 command: vec![
1174 "less".to_owned(),
1175 "-F".to_owned(),
1176 "-R".to_owned(),
1177 "-X".to_owned()
1178 ],
1179 env: BTreeMap::new(),
1180 }
1181 );
1182 assert_eq!(wrapper.cmd.command_name(), "less");
1183 assert_eq!(
1184 wrapper.cmd.args(),
1185 &["-F".to_owned(), "-R".to_owned(), "-X".to_owned()]
1186 );
1187
1188 let cmd: CommandNameAndArgs = toml::from_str(
1190 r#"
1191 command = ["less", "-FRX"]
1192 env = { LESSCHARSET = "utf-8" }
1193 "#,
1194 )
1195 .unwrap();
1196 let expected_env: BTreeMap<String, String> =
1197 [("LESSCHARSET".to_owned(), "utf-8".to_owned())]
1198 .into_iter()
1199 .collect();
1200 assert_eq!(
1201 cmd,
1202 CommandNameAndArgs {
1203 command: vec!["less".to_owned(), "-FRX".to_owned()],
1204 env: expected_env,
1205 }
1206 );
1207 assert_eq!(cmd.command_name(), "less");
1208 assert_eq!(cmd.args(), &["-FRX".to_owned()]);
1209
1210 let wrapper: Wrapper = toml::from_str(r#"cmd = 'my-pager "arg with spaces"'"#).unwrap();
1212 assert_eq!(
1213 wrapper.cmd,
1214 CommandNameAndArgs {
1215 command: vec!["my-pager".to_owned(), "arg with spaces".to_owned()],
1216 env: BTreeMap::new(),
1217 }
1218 );
1219
1220 let wrapper: Wrapper = toml::from_str(r#"cmd = "my-pager 'arg with spaces'""#).unwrap();
1222 assert_eq!(
1223 wrapper.cmd,
1224 CommandNameAndArgs {
1225 command: vec!["my-pager".to_owned(), "arg with spaces".to_owned()],
1226 env: BTreeMap::new(),
1227 }
1228 );
1229
1230 let wrapper: Wrapper =
1232 toml::from_str(r#"cmd = 'my-pager "quoted \"nested\" arg"'"#).unwrap();
1233 assert_eq!(
1234 wrapper.cmd,
1235 CommandNameAndArgs {
1236 command: vec!["my-pager".to_owned(), "quoted \"nested\" arg".to_owned()],
1237 env: BTreeMap::new(),
1238 }
1239 );
1240
1241 let wrapper: Wrapper = toml::from_str(r#"cmd = '"/path/to/my pager" --flag'"#).unwrap();
1243 assert_eq!(
1244 wrapper.cmd,
1245 CommandNameAndArgs {
1246 command: vec!["/path/to/my pager".to_owned(), "--flag".to_owned()],
1247 env: BTreeMap::new(),
1248 }
1249 );
1250
1251 let wrapper: Wrapper =
1253 toml::from_str(r#"cmd = 'cmd "first arg" "second arg" third'"#).unwrap();
1254 assert_eq!(
1255 wrapper.cmd,
1256 CommandNameAndArgs {
1257 command: vec![
1258 "cmd".to_owned(),
1259 "first arg".to_owned(),
1260 "second arg".to_owned(),
1261 "third".to_owned(),
1262 ],
1263 env: BTreeMap::new(),
1264 }
1265 );
1266 }
1267
1268 #[test]
1269 fn test_command_and_pager_empty_errors() {
1270 #[derive(Debug, Deserialize)]
1271 struct Wrapper {
1272 #[expect(dead_code)]
1273 cmd: CommandNameAndArgs,
1274 }
1275
1276 let cmd_cases = [
1278 ("empty array", "cmd = []"),
1279 ("empty string", r#"cmd = """#),
1280 ("whitespace-only string", r#"cmd = " ""#),
1281 (
1282 "structured with empty command",
1283 r#"cmd = { command = [], env = { LESSCHARSET = "utf-8" } }"#,
1284 ),
1285 ];
1286
1287 for (name, input) in cmd_cases {
1288 let err = toml::from_str::<Wrapper>(input).unwrap_err();
1289 assert!(
1290 err.to_string().contains("must not be empty"),
1291 "CommandNameAndArgs {name}: error should mention 'must not be empty': {err}"
1292 );
1293 }
1294
1295 let pager_cases = [
1297 ("empty array", "pager = []"),
1298 ("empty string", r#"pager = """#),
1299 ];
1300
1301 for (name, input) in pager_cases {
1302 let err = toml::from_str::<DeserializedUiConfig>(input).unwrap_err();
1303 assert!(
1304 err.to_string().contains("must not be empty"),
1305 "PagerSetting {name}: error should mention 'must not be empty': {err}"
1306 );
1307 }
1308
1309 let unclosed_quote_cases = [
1311 ("unclosed double quote", r#"cmd = 'pager "unclosed'"#),
1312 ("unclosed single quote", r#"cmd = "pager 'unclosed""#),
1313 ];
1314
1315 for (name, input) in unclosed_quote_cases {
1316 let err = toml::from_str::<Wrapper>(input).unwrap_err();
1317 assert!(
1318 err.to_string().contains("missing closing quote"),
1319 "CommandNameAndArgs {name}: error should mention 'missing closing quote': {err}"
1320 );
1321 }
1322 }
1323
1324 #[test]
1325 fn test_command_name_and_args_to_command() {
1326 let cmd = CommandNameAndArgs {
1328 command: vec!["echo".to_owned(), "hello".to_owned()],
1329 env: BTreeMap::new(),
1330 };
1331 let std_cmd = cmd.to_command();
1332 assert_eq!(cmd.command_name(), "echo");
1333 drop(std_cmd);
1334 }
1335
1336 #[test]
1337 fn test_pager_setting_parsing() {
1338 let config: DeserializedUiConfig = toml::from_str(r#"pager = "less -FRX""#).unwrap();
1340 assert_eq!(
1341 config.pager,
1342 Some(PagerSetting::External(CommandNameAndArgs {
1343 command: vec!["less".to_owned(), "-FRX".to_owned()],
1344 env: BTreeMap::new(),
1345 }))
1346 );
1347
1348 let config: DeserializedUiConfig = toml::from_str(r#"pager = ["less", "-FRX"]"#).unwrap();
1350 assert_eq!(
1351 config.pager,
1352 Some(PagerSetting::External(CommandNameAndArgs {
1353 command: vec!["less".to_owned(), "-FRX".to_owned()],
1354 env: BTreeMap::new(),
1355 }))
1356 );
1357
1358 let config: DeserializedUiConfig = toml::from_str(
1360 r#"
1361 [pager]
1362 command = ["less", "-FRX"]
1363 env = { LESSCHARSET = "utf-8" }
1364 "#,
1365 )
1366 .unwrap();
1367 let expected_env: BTreeMap<String, String> =
1368 [("LESSCHARSET".to_owned(), "utf-8".to_owned())]
1369 .into_iter()
1370 .collect();
1371 assert_eq!(
1372 config.pager,
1373 Some(PagerSetting::External(CommandNameAndArgs {
1374 command: vec!["less".to_owned(), "-FRX".to_owned()],
1375 env: expected_env,
1376 }))
1377 );
1378
1379 let config: DeserializedUiConfig = toml::from_str("").unwrap();
1381 assert!(config.pager.is_none());
1382 }
1383
1384 #[test]
1385 fn test_resolved_ui_config_pager_defaults() {
1386 let defaults = DefaultUserConfig::from_embedded().ui;
1387
1388 let build_target =
1389 Platform::build_target().expect("nextest is built for a supported platform");
1390 let resolved = UiConfig::resolve(&defaults, &[], None, &[], &build_target);
1391
1392 assert_eq!(resolved.pager, defaults.pager);
1394 assert_eq!(resolved.paginate, defaults.paginate);
1395 }
1396
1397 #[test]
1398 fn test_resolved_ui_config_pager_override() {
1399 let defaults = DefaultUserConfig::from_embedded().ui;
1400
1401 let custom_pager = PagerSetting::External(CommandNameAndArgs {
1403 command: vec!["more".to_owned()],
1404 env: BTreeMap::new(),
1405 });
1406 let override_ = make_override(
1407 "cfg(all())",
1408 DeserializedUiOverrideData {
1409 pager: Some(custom_pager.clone()),
1410 ..Default::default()
1411 },
1412 );
1413
1414 let build_target =
1415 Platform::build_target().expect("nextest is built for a supported platform");
1416 let resolved = UiConfig::resolve(&defaults, &[], None, &[override_], &build_target);
1417
1418 assert_eq!(resolved.pager, custom_pager);
1419 assert_eq!(resolved.paginate, defaults.paginate);
1421 }
1422
1423 #[test]
1424 fn test_resolved_ui_config_paginate_override() {
1425 let defaults = DefaultUserConfig::from_embedded().ui;
1426
1427 let override_ = make_override(
1429 "cfg(all())",
1430 DeserializedUiOverrideData {
1431 paginate: Some(PaginateSetting::Never),
1432 ..Default::default()
1433 },
1434 );
1435
1436 let build_target =
1437 Platform::build_target().expect("nextest is built for a supported platform");
1438 let resolved = UiConfig::resolve(&defaults, &[], None, &[override_], &build_target);
1439
1440 assert_eq!(resolved.paginate, PaginateSetting::Never);
1441 assert_eq!(resolved.pager, defaults.pager);
1443 }
1444
1445 #[test]
1446 fn test_pager_setting_builtin() {
1447 let config: DeserializedUiConfig = toml::from_str(r#"pager = ":builtin""#).unwrap();
1449 assert_eq!(config.pager, Some(PagerSetting::Builtin));
1450 }
1451
1452 #[test]
1453 fn test_streampager_config_parsing() {
1454 let config: DeserializedUiConfig = toml::from_str(
1456 r#"
1457 [streampager]
1458 interface = "full-screen-clear-output"
1459 wrapping = "anywhere"
1460 show-ruler = false
1461 "#,
1462 )
1463 .unwrap();
1464 assert_eq!(
1465 config.streampager.interface,
1466 Some(StreampagerInterface::FullScreenClearOutput)
1467 );
1468 assert_eq!(
1469 config.streampager.wrapping,
1470 Some(StreampagerWrapping::Anywhere)
1471 );
1472 assert_eq!(config.streampager.show_ruler, Some(false));
1473
1474 let config: DeserializedUiConfig = toml::from_str(
1476 r#"
1477 [streampager]
1478 interface = "quit-quickly-or-clear-output"
1479 "#,
1480 )
1481 .unwrap();
1482 assert_eq!(
1483 config.streampager.interface,
1484 Some(StreampagerInterface::QuitQuicklyOrClearOutput)
1485 );
1486 assert_eq!(config.streampager.wrapping, None);
1487 assert_eq!(config.streampager.show_ruler, None);
1488
1489 let config: DeserializedUiConfig = toml::from_str("").unwrap();
1491 assert_eq!(config.streampager.interface, None);
1492 assert_eq!(config.streampager.wrapping, None);
1493 assert_eq!(config.streampager.show_ruler, None);
1494 }
1495
1496 #[test]
1497 fn test_streampager_config_resolution() {
1498 let defaults = DefaultUserConfig::from_embedded().ui;
1499
1500 let override_ = make_override(
1502 "cfg(all())",
1503 DeserializedUiOverrideData {
1504 streampager: DeserializedStreampagerConfig {
1505 interface: Some(StreampagerInterface::FullScreenClearOutput),
1506 wrapping: None,
1507 show_ruler: None,
1508 },
1509 ..Default::default()
1510 },
1511 );
1512
1513 let build_target =
1514 Platform::build_target().expect("nextest is built for a supported platform");
1515 let resolved = UiConfig::resolve(&defaults, &[], None, &[override_], &build_target);
1516
1517 assert_eq!(
1519 resolved.streampager.interface,
1520 StreampagerInterface::FullScreenClearOutput
1521 );
1522 assert_eq!(resolved.streampager.wrapping, defaults.streampager.wrapping);
1524 assert_eq!(
1525 resolved.streampager.show_ruler,
1526 defaults.streampager.show_ruler
1527 );
1528 }
1529}