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